fandhe-frontend-headless-ui API
1. 目的とトレーサビリティ
本ドキュメントは fandhe-frontend-headless-ui(ark-ui / chakra-ui 参考の 2 層 UI コンポーネント構成)が提供する headless(unstyled)UI コンポーネント層の公開 API 表面をまとめる。上層の fandhe-frontend-pre-styled-ui(chakra-ui 相当)は本層の anatomy・data-*・WAI-ARIA 出力を前提にスタイルを重ねる。
docs/api/component-api.md との整合: 本クレートのコンポーネントはすべて「fandhe_frontend_core::Node を返す通常の Rust 関数」(REQ-5 の凍結 API 前提)として実装され、fandhe_frontend_core::render の既定エスケープ(REQ-1)を必ず経由する。raw_html() は使用しない。
2. 位置づけ
- headless-ui は anatomy・
data-*・WAI-ARIA を出力する下層、pre-styled-ui は本層の出力を前提にスタイルを重ねる上層という 2 層構成である。 - 外部依存は
fandhe-frontend-core/fandhe-frontend-interactive(いずれも crates.io バージョン依存)のみ
3. 共通基盤 API
| モジュール/型 | 役割 |
|---|---|
anatomy::Anatomy / anatomy::anatomy | data-scope/data-part を付与してパーツノード(div/button/span/input 等)を組み立てる。全コンポーネント共通の anatomy 基盤 |
data_attrs | data-state/data-disabled/data-invalid/data-orientation/data-readonly/data-required 等の状態属性ヘルパ。Orientation enum(Horizontal/Vertical) |
aria | role/aria-*(aria_checked/aria_controls/aria_describedby/aria_disabled/aria_expanded/aria_haspopup/aria_hidden/aria_invalid/aria_label/aria_labelledby/aria_modal/aria_multiselectable/aria_orientation/aria_selected)の WAI-ARIA 属性ヘルパ |
state::OpenState | Open/Closed の 2 値状態(Default は Closed。SSR の状態なし初期描画に対応)。as_data_state()/is_open()/toggled() |
state::Disclosure / state::DisclosureAction | 単一の開閉状態機械。fandhe_frontend_interactive::Component/Hydrate を実装し、dispatch アクション名 "open"/"close"/"toggle" を受理する |
state::SingleSelect / state::SingleSelectAction | 「高々 1 項目が選択される」状態機械(Accordion の single モード等が使用)。dispatch アクション名 "select"/"deselect"/"toggle" |
state::TextInput / state::TextInputAction | 自由入力文字列 1 個を持つ状態機械(Combobox が使用)。dispatch アクション名 "input"/"clear" |
state::pressed_data_state / state::DATA_STATE_ON / state::DATA_STATE_OFF | Toggle/ToggleGroup が使う「押下状態」の data-state 値語彙("on"/"off")。state::Checkable(checked/unchecked)を埋め込みつつも公開語彙を分離するための変換関数 |
これらは Dialog / Accordion / Tabs / Collapsible / Popover / Tooltip が共通で使う「open/closed・selected」の dispatch 契約・data-state 整合・SSR/hydration 契約を一度だけ実装したものであり、各コンポーネントはフィールドとして埋め込んで再利用する。
4. コンポーネント一覧
| コンポーネント | モジュール | anatomy パーツ | 埋め込む状態機械 | |
|---|---|---|---|---|
| Collapsible | collapsible | Root/Trigger/Indicator/Content | state::Disclosure。ark-ui/Radix Primitives と参照突合し、indicator/content へ disabled: bool 位置引数を新設して 4 パートすべてへ data-disabled を反映するようにした(ネイティブ disabled 存在属性は trigger のみ)。呼び出し側 attrs による data-state/data-disabled/aria-expanded 等の偽装は予約キー除去で fail-closed に防止する(イシュー #1637) | |
| Accordion(single モード) | accordion | Root/Item/ItemTrigger/ItemIndicator/ItemContent | state::SingleSelect。全パーツが AccordionProps(orientation/disabled、イシュー #1636)を受け取り data-orientation を全パーツへ、実効 disabled を item-trigger/item-indicator/item-content へ反映する。item-trigger の aria-disabled(disabled 時のみ)・item-indicator の常時 aria-hidden="true" も同イシューで追加。キーボード(vertical: ArrowDown/ArrowUp、horizontal: ArrowRight/ArrowLeft、Home/End、非循環)の DOM 配線は fandhe-frontend-wasm-full の keynav.rs::accordion_next_index_oriented | |
| Tabs | tabs | Root/List/Trigger/Content/Indicator(opt-in、#601。自由関数 tabs()、SSR 静的選択状態のみ) | なし(クリック/dispatch は wasm 層のスコープ)。イシュー #1656 で ark-ui / Radix Primitives / Radix Themes / chakra-ui と突合済み: 是正なし、パート・data-* 増減なし。Radix の data-* 表と属性単位で一致。ark の data-selected/data-focus/data-ssr は非採用(data-state 語彙で充足・実行時状態は SSR の責務外)。キーボードは fandhe-frontend-wasm-full の keynav.rs::tabs_next_index/handle_tabs_keydown/activate_tab が実装済み(#582) | |
| Tooltip | tooltip | Root/Trigger/Positioner/Content/Arrow/ArrowTip | state::Disclosure。イシュー #1645 で ark-ui / Radix Primitives / chakra-ui と突合済み: 是正なし。パート・data-* 増減なし。Tab(focus)/Escape は fandhe-frontend-wasm-full(tooltip::wiring / overlay)が実装済み | |
| Dialog | dialog | Root/Trigger/Backdrop/Positioner/Content/Title/Description/CloseTrigger | state::Disclosure。イシュー #1638 で ark-ui(zag dialog.connect.ts)/Radix Primitives/chakra-ui と突合し、content に tabindex="-1" を固定付与。Escape 閉鎖・外側クリック閉鎖・フォーカストラップは fandhe-frontend-wasm-full(overlay/focus_trap/headless)が実装済み。イシュー #2193 で close_trigger_with_variant(CloseTriggerVariant, attrs, children) を追加し、data-variant(icon/text)を選択できるようにした(既存 close_trigger は icon 契約のまま不変) | |
| Popover | popover | Root/Trigger/Anchor/Positioner/Arrow/ArrowTip/Content/Title/Description/CloseTrigger/Indicator | state::Disclosure。イシュー #1642 で ark-ui / Radix Primitives / chakra-ui と突合し、content に tabindex="-1" を固定付与。Escape・外側クリック閉鎖は fandhe-frontend-wasm-full(overlay/headless)が実装済み、フォーカストラップ・復帰は無し | |
| RadioGroup | radio_group | Root/Label/Item/ItemControl/ItemText/ItemHiddenInput | state::SingleSelect。RadioGroupProps(disabled/readonly/invalid/required)を root/label へ data-disabled/data-invalid/data-required として、item 系 3 パーツへ data-disabled/data-readonly/data-invalid として反映する(パート別の反映属性は ark-ui の Data Attributes 表準拠、data-readonly は root へ出力しない)。root は disabled/readonly/required に応じ aria-disabled/aria-readonly/aria-required(true 時のみ)も付与する。item_control は aria-hidden="true" を常時付与し、item_hidden_input は required(props.required)/aria-invalid="true"(props.invalid)を出力する(イシュー #1616) | |
| Switch | switch | Root/Control/Thumb/Label/HiddenInput | 独自実装("checked"/"unchecked" 語彙が Disclosure と異なるため Component/Hydrate を直接実装)。イシュー #1622 で ark-ui / Radix Primitives と突合し SwitchProps(disabled/readonly/invalid/required)を新設。全 5 パーツへ data-disabled/data-invalid/data-required/data-readonly を反映し、hidden_input は aria-invalid="true"(invalid 時)も出力する。呼び出し側 attrs による data-state 等の偽装は予約キー除去で fail-closed に防止する | |
| Field | field | Root/Label/Input/Textarea/Select/HelperText/ErrorText/RequiredIndicator/Group/Content/Title/Separator/SeparatorLine/SeparatorContent | なし(invalid/disabled/required/readonly は SSR 静的な props)。イシュー #2185 で Group/Content/Title/Separator(内部パーツ SeparatorLine/SeparatorContent)の 6 パーツを純追加(shadcn/ui FieldGroup/FieldContent/FieldTitle/FieldSeparator 相当、既存 8 パーツのシグネチャ・出力は不変)。Group/SeparatorLine は role="group"/role="separator"(+ aria-orientation="horizontal")を固定付与し、Title は for/id を自動導出しない(呼び出し側が attrs で id を渡し対応コントロールへ aria-labelledby で結び付ける運用) | |
| Menu | menu | Root/Trigger/Indicator/Positioner/Content/Arrow/ArrowTip/Item/ItemText/ItemIndicator/ItemGroup/ItemGroupLabel/Separator/TriggerItem/ContextTrigger/CheckboxItem/RadioItemGroup/RadioItem | state::Disclosure(+ Checkable/SingleSelect を CheckboxItem/RadioItemGroup へ独立に埋め込む)。イシュー #1651 で ark-ui / Radix Primitives / chakra-ui と突合し ItemText/ItemIndicator の 2 パーツを追加(16 → 18)、呼び出し側 attrs の予約キー除去を追加 | |
| Select | select | Root/Label/Control/Trigger/ValueText/ClearTrigger/Indicator/Positioner/Content/ItemGroup/ItemGroupLabel/Item/ItemText/ItemIndicator/HiddenSelect/Separator/ScrollUpButton/ScrollDownButton | state::Disclosure + state::SingleSelect(開閉 + 選択値の合成)。SelectProps(disabled/readonly/invalid/required)を root/label/control/trigger/value-text/clear-trigger/indicator/item-group へ一律付与し、trigger の data-placeholder-shown・item の root disabled 伝播と data-selected・item-text の 3 状態属性・item-group-label の role="presentation"・item-indicator の aria-hidden="true" を追加する(ark-ui/Radix との参照突合、イシュー #1619)。イシュー #2186 で Separator(aria-hidden="true")・ScrollUpButton/ScrollDownButton(aria-hidden="true")の 3 パーツを追加(15 → 18)。Separator は当初 role="separator"/aria-orientation を付与していたが、content(role="listbox")の aria-required-children(子は option/group のみ)制約違反のため aria-hidden のみへ改めた(Bugbot 指摘対応)。可視性判定・押下スクロールは wasm-full の後続イシュー | |
| Avatar | avatar | Root/Image/Fallback | 独自実装("loading"/"loaded"/"error" の 3 値ステータス、ImageStatus)。参考サイト突合(イシュー #1659): ark-ui/Zag.js・Radix Primitives・Radix Themes・chakra-ui の 4 参照サイトと突合し、anatomy/data-state 語彙/ARIA(非付与)とも一致、是正なし。Radix Primitives の Fallback delayMs(表示遅延)・各パーツへの dir/id・chakra-ui のイニシャル自動導出はいずれも意図的に非採用(docs/policy/intentional-non-adoption.md §3.25 規則 2 等)。参照 4 サイトともキーボード操作の記載なし | |
| NumberInput | number_input | Root/Label/Control/Input/IncrementTrigger/DecrementTrigger/ValueText | 独自実装(連続量の値のため data-state を持たず Component/Hydrate を直接実装。数値整形・パースはロケール非依存で決定的、step 演算は小数桁への丸めで浮動小数点ドリフトを防ぐ。ValueText・data-readonly/data-required・"home"/"end" dispatch はイシュー #1613 で追加) | |
| PasswordInput | password_input | Root/Label/Control/Input/VisibilityTrigger/Indicator | 独自実装("visible"/"hidden" 語彙が Checkable と異なるため Component/Hydrate を直接実装、PasswordInput)。パスワード値そのものは一切扱わない(§6 参照) | |
| Slider | slider | Root/Label/Control/Track/Range/Thumb/HiddenInput/ValueText | 独自実装(連続量の値のため data-state を持たず Component/Hydrate を直接実装。value は常に min 起点で step 単位へスナップしてから [min, max] へ clamp する。thumb が role="slider" + aria-valuemin/max/now/aria-orientation を担う) | |
| PinInput | pin_input | Root/Label/Control/Input/HiddenInput | 独自実装(固定桁数の文字配列 + フォーカス位置、Disclosure/SingleSelect の語彙に収まらないため Component/Hydrate を直接実装。イシュー #1615 で ark-ui/Radix 参照突合、§6-10 参照) | |
| Questionnaire(Primitives、イシュー #2117。Themes ページは #2119 で追加) | questionnaire | Root/Progress/Question/Prompt/Description/Options/Freeform/Actions/Back/Next/Skip | 独自実装(Steps 同型の count/step 状態機械、data-state=active/completed/upcoming。回答値の保持・検証・分岐・送信はアプリ責務のため持たない、.claude/rules/coding-rust.md §UI 部品の責務境界 規則 1) | |
| Data Table(Primitives、イシュー #2125。Themes は #2127 で追加) | data_table | Root/Toolbar/ColumnHeader/SortTrigger/SelectAll/SelectRow/Footer/SelectionCount | 独自実装(sort + hidden-columns の最小状態機械、選択集合・並べ替え・取得はアプリ責務。<table>/<thead>/<tbody>/<tr> は生成せず、column_attrs/column_header_attrs/row_attrs の属性ヘルパで pre-styled table::* へパススルーする設計、.claude/rules/coding-rust.md §UI 部品の責務境界 規則 1) | |
| TagsInput | tags_input | Root/Label/Control/Input/Item/ItemPreview/ItemText/ItemInput/ItemDeleteTrigger/ClearTrigger/HiddenInput/LiveRegion | 独自実装(可変長タグ文字列リスト + 編集中/キーボード強調インデックス、SingleSelect/MultiSelect の語彙に収まらないため Component/Hydrate を直接実装。TagsInputProps(disabled/readonly/invalid/required)・TagItem(value/disabled/editing/highlighted)が各パーツへ状態束を一律付与する。control/item-preview は role を持たない(イシュー #1623 で zag/ark 準拠へ是正、旧実装の role="listbox"/role="option" は撤去)。highlight-prev/highlight-next/highlight-clear/delete-highlighted/backspace dispatch(DOM 配線は wasm-full の別イシュー)を新設。live_region はタグ数変化の通知用 live region、aria-live="polite" 固定・テキスト更新は wasm-full の後続責務、イシュー #1069) | |
| RatingGroup | rating_group | Root/Label/Control/Item/HiddenInput | 独自実装(1..=count の数値評価値 + hover プレビューを持つ。hover は SSR 非活性・hydration 非直列化。Component/Hydrate を直接実装。イシュー #1617 で ark-ui 参照突合、§6-11 参照) | |
| Editable | editable | Root/Label/Area/Input/Preview/Control/EditTrigger/SubmitTrigger/CancelTrigger | 独自実装("preview"/"edit" の 2 モードが Disclosure/SingleSelect の語彙に収まらないため Component/Hydrate を直接実装。mode == Preview のとき常に draft == value を保つ不変条件を持つ。イシュー #1606 で参照突合(EditableInputFlags 共有・data-invalid/data-required・preview tabindex/aria-*・activation/submit none 追加。data-focus/data-autoresize/DOM 配線は見送り)) | |
| Toggle | toggle | Root/Indicator | state::Checkable(data-state 語彙は "on"/"off"。checked_data_state ではなく state::pressed_data_state で変換し、Switch の "checked"/"unchecked" と分離する)。indicator は data-state/data-pressed/data-disabled を出力する(イシュー #1629) | |
| ToggleGroup(single モード) | toggle_group | Root/Item | state::SingleSelect(dispatch は "toggle" のみ受理、常時 deselectable)。root/item は共通の ToggleGroupProps(disabled/orientation/roving_focus)を受け取り、disabled は item へ伝播、orientation が Some のとき item にも data-orientation を付与、roving_focus(既定 false)で SSR 側 roving tabindex を opt-in 出力する(イシュー #1630) | |
| MultiToggleGroup(multiple モード) | toggle_group | Root/Item | state::MultiSelect(dispatch は "toggle" のみ受理)。ToggleGroupProps の契約は上記 ToggleGroup と同じ | |
| SegmentGroup | segment_group | Root/Indicator/Item/ItemText/ItemControl/ItemHiddenInput | radio_group::RadioGroup(state::SingleSelect)へ全委譲(独自の状態機械を新設せず、既存 RadioGroup の dispatch/hydration をそのまま再利用する)。参照突合(イシュー #1618): SegmentGroupProps(disabled/readonly/invalid/required)を新設し RadioGroupProps と同じパート別反映契約(root に data-disabled/data-invalid/data-required+対応する aria-*、indicator に data-disabled、item 系 3 パーツに data-disabled/data-readonly/data-invalid、item-hidden-input に required/aria-invalid)を適用。item-control へ aria-hidden="true" を常時付与。ark の Label パーツ(Anatomy 図に無い)・fandhe-frontend-wasm-full の CSR 配線(radio_group と異なり未着手)は据え置き | |
| Listbox / MultiListbox | listbox | Root/Label/Content/ItemGroup/ItemGroupLabel/Item/ItemText/ItemIndicator/ValueText | state::SingleSelect(Listbox)/ state::MultiSelect(MultiListbox)へ全委譲。常時展開(trigger/positioner なし)で Select とは責務境界が異なる(詳細は listbox モジュール doc 参照)。イシュー #1611 で参照突合: ListboxProps(disabled/orientation)を Root/Label/Content/ItemGroup/Item/ValueText へ共有し、Root の disabled が Item の有効 disabled へ伝播する。Item は選択時のみ data-selected 存在属性を追加、ItemText は Item と同型の 3 状態属性を出力、ItemGroupLabel は role="presentation"、ItemIndicator は aria-hidden="true" を固定付与する | |
| Combobox | combobox | Root/Label/Control/Input/Trigger/ClearTrigger/Positioner/Content/ItemGroup/ItemGroupLabel/Item/ItemText/ItemIndicator/LiveRegion | state::Disclosure + state::SingleSelect + state::TextInput(開閉 + 選択値 + 入力値の合成)。ARIA 1.2 combobox パターンに準拠し aria-activedescendant は content ではなく input 側に配線する(Select との差異)。live_region は候補件数変化の通知用 live region、aria-live="polite" 固定・テキスト更新は wasm-full の後続責務、イシュー #1069。参照突合(イシュー #1605): ComboboxProps(disabled/readonly/invalid/required)を root/label/control/input/trigger/clear_trigger へ一律付与(ark-ui superset)、data-required は label のみ、input は invalid 時のみ aria-invalid="true" を追加。ark-ui の item/item-indicator data-state="checked"/"unchecked" 語彙・data-focus/data-placement/data-empty・List/Empty パーツは select/listbox とのクレート横断整合を優先し意図的に非追随 | |
| Command(Primitives、イシュー #2068。Themes ページは #2070 で追加) | command | Root/Input/List/Empty/Group/GroupHeading/Item/Shortcut/Separator/Dialog | state::Disclosure(dialog 開閉)+ state::TextInput(検索クエリ)+ state::SingleSelect(選択中 item の value)。combobox/listbox の既存 ARIA 実装(role="combobox"/role="listbox"/role="option")を再利用し、aria-controls/aria-label を必須引数(Option opt-in にしない)にすることで docs-site の R1〜R4 契約を型で構造的に満たす。item は data-selected(presence)で選択有無を表し data-highlighted/data-state は出力しない(combobox/listbox との意図的な差分)。Input は dialog を開閉せず、Select は dialog を閉じない(cmdk のコマンドパレット意味論、実行フックは UI コンポーネント層の範囲外)。filter_items は combobox::filter_options へ全委譲する。実 DOM 上の入力絞り込み反映・矢印キーによる行選択・Enter による実行フック("command:execute")・Cmd/Ctrl+K での dialog 開閉は fandhe-frontend-wasm-full の command モジュール(イシュー #2069)が実装済み。shadcn/ui のみに存在する部品(参照軸 #2001) | |
| Steps | steps | Root/List/Item/Trigger/Indicator/Separator/Content/CompletedContent/PrevTrigger/NextTrigger/Progress | 独自実装(count(全 step 数)+ step(現在位置、0..=count)を持つ。item は complete/current/incomplete の 3 状態、current な item の trigger のみ aria-current="step"。Disclosure/SingleSelect の語彙に収まらないため Component/Hydrate を直接実装)。参考サイト突合(イシュー #1665): ark-ui/Zag.js(steps machine)・chakra-ui との突合で prev_trigger/next_trigger へ data-disabled を、trigger/content/completed_content へ data-orientation を追加し、Zag.js anatomy に存在し欠けていた progress(role="progressbar" + aria-valuemin/aria-valuemax/aria-valuenow/aria-valuetext)パーツを新設した(いずれも既存シグネチャを変えない加算のみ)。trigger の `data-state="open"\ | "closed"・aria-current の item への付与・tabs 意味論(role="tablist"/"tab"/"tabpanel" + id 配管)・indicator の aria-hidden・root の style="--percent"/dir・data-skippable は golden CSS 破壊・支援技術での重複読み上げ・破壊的シグネチャ変更・Themes 層の関心のいずれかを理由に意図的に非採用(crate::steps モジュール doc「参照突合」節参照)。fandhe-frontend-wasm-full の headless::MAPPING_TABLE に "steps"` scope は無く、trigger/prev-trigger/next-trigger の click・キー操作は未配線(ネイティブ button の Tab/Enter/Space のみ機能する) |
| TreeView | tree_view | Root/Label/Tree/Branch/BranchControl/BranchIndicator/BranchText/BranchContent/BranchIndentGuide/Item/ItemText/ItemIndicator | state::MultiSelect(展開中のブランチ値の集合)+ state::SingleSelect(選択中のノード値)の合成。両者とも hydration_attrs のフィールド名が "selected" で衝突するため、展開集合側のみ "expanded" へ書き換えて運ぶ(tree_view モジュール doc §hydration フィールド名参照)。TreeView::render_nodes が TreeNode 列から深さ・aria-posinset/aria-setsize を再帰的に計算する。ark-ui docs / zag.js との参照突合(イシュー #1667)でノード共通プロパティを TreeItemProps 構造体へ集約するシグネチャ変更(破壊的変更)を行い、branch の data-branch・branch-control/branch-content の data-value/data-depth・インジケータ/テキスト系の data-selected/data-disabled/data-state・item-indicator の非選択時 hidden/aria-hidden="true" を追加した。キーボードナビゲーション(矢印キー・Home/End・Enter/Space・typeahead)の DOM 配線は fandhe-frontend-wasm-full の keynav.rs §TreeView(イシュー #1072)が担う | |
| Pagination | pagination | Root/Item/Ellipsis/PrevTrigger/NextTrigger/FirstTrigger/LastTrigger | 独自実装(総件数・ページサイズ・現在ページ・sibling/boundary 件数から省略記号を含むページ列を導出する page_range(決定的・O(boundary_count + sibling_count))+ Component/Hydrate を直接実装する値状態機械。現在ページは aria-current="page"/data-selected で、端到達は disabled/data-disabled で表現する。item は data-index(ページ番号)を常時出力し、dispatch は "goto"/"next"/"prev"/"first"/"last" を受理する。参照突合(イシュー #1655): ark-ui との突合で FirstTrigger/LastTrigger パーツと item の data-index を追加、呼び出し側 attrs の予約キー除去(drop_reserved)を追加。キーボードの独自実装・chakra-ui の PageText 数値整形は意図的に非追随) | |
| Breadcrumb | breadcrumb | Root/List/Item/Link/CurrentLink/Separator/Ellipsis | なし(自由関数のみ、SSR 静的な意味論ナビ。現在位置は aria-current="page" + data-current の併用で表現。イシュー #1648 で chakra-ui 参照突合。anatomy 7 パーツ・ARIA 一致、data-*/パート増減なし。呼び出し側 attrs の予約キー除去を追加。イシュー #2182 で shadcn/ui BreadcrumbPage の role="link"/aria-disabled="true" は不採用と判断) | |
| Button Group(Themes、イシュー #2059。Themes ページは #2060) | button_group | Root/Separator/Text | なし(自由関数のみ。role="group" の静的なグループ化で toolbar の roving tabindex 状態機械とは異なる。role="group" へ aria-orientation は付与せず data-orientation のみで向きを表現。参照実体は shadcn/ui のみ〔参照軸 #2001〕。呼び出し側 attrs の予約キー除去あり) | |
| Item(Primitives、イシュー #2065。Themes ページは #2066 で追加) | item | Root/Media/Content/Title/Description/Actions/Header/Footer/Group/Separator | なし(自由関数のみ。root は href 指定で a 描画・external で target/rel 不可分付与。group は role="group"〔shadcn の role="list" からの意図的差分〕。参照実体は shadcn/ui のみ〔#2001〕。予約キー除去あり) | |
| Sidebar(Primitives、イシュー #2072。Themes 層 recipe は #2073、Themes ページは #2075 で追加) | sidebar | Provider/Root/Header/Content/Footer/Separator/Input/Group/GroupLabel/GroupContent/GroupAction/Menu/MenuItem/MenuButton/MenuAction/MenuBadge/MenuSub/MenuSubItem/MenuSubButton/Rail/Trigger/Inset | Sidebar 状態機械(独自実装、`data-state="expanded"\ | "collapsed"。Disclosure とは語彙が異なるため独立実装)。nav_list(menu/menu-item/menu-button の構造・aria-current 語彙)・collapsible(menu-sub の開閉は呼び出し側が合成、他 scope を内包しない)・tooltip(menu-button の describedby のみで関連付け)の語彙を再利用する。data-collapsible は状態に関わらず常時出力(SSR 決定性優先の意図的差分)。inset は div(main ではない。docs サイト骨格の main と入れ子にならないための意図的差分)。menu-skeleton は非採用(ランダム幅で SSR 決定性を壊すため pre-styled-ui 側 #2073 が [sidebar::menu_skeleton`] として決定的な固定幅版を提供)。shadcn/ui のみに存在する部品(参照軸 #2001) |
| Message(Primitives、イシュー #2105。Themes ページは #2106 で追加) | message | Root/Avatar/Header/Content/Footer/Group | なし(自由関数のみ、状態機械を持たない静的部品)。root は role="listitem"、group は role="list" + 任意 aria-label を固定付与(a[href] を持たないため item の role="group" 差分化とは異なる)。会話系 4 部品(message/bubble/attachment/marker)が共有する data-role(user/assistant/system)・data-align(start/end、data-role から独立した軸)・data-loading/data-error(存在属性)の共通語彙を本モジュールが最初に確定する。aria-live/aria-busy は付与しない(ストリーミング通知・応答待ちの読み上げはアプリ責務)。avatar はスロットで既存 avatar mod を内包しない。参照実体は shadcn/ui のみ(参照軸 #2001) | |
| Message Scroller(Primitives、イシュー #2121。Themes は #2123 で追加) | message_scroller | Root/Viewport/Content/Anchor/JumpToLatest/LoadMore | なし(自由関数のみ、状態機械を持たない静的部品)。root は data-stuck(bottom/free)・data-has-new(存在属性)を出力。viewport は scroll_area::viewport と同じ tabindex="0" 固定契約を持つが scroll_area へは委譲せず message-scroller scope 自身のパーツとして独立実装。content は role="log" を固定付与しない純スロット。anchor は viewport 末尾の aria-hidden="true" センチネル(wasm-full 側の観測対象、#2122)。jump-to-latest/load-more の可視・読み込み表示のみを担い、最下部追従・新着検知・スクロール位置の計測復元は一切内包しない(配線は #2122)。aria-live/aria-busy/aria-posinset/aria-setsize は付与しない。参照実体は shadcn/ui のみ(参照軸 #2001) | |
| Bubble(Primitives、イシュー #2108。Themes ページは #2109 で追加) | bubble | Root/Content/Reactions/Reaction/CollapseTrigger/CollapseContent | なし(自由関数のみ、状態機械を持たない静的部品)。data-align は message::MessageAlign を再利用する(会話系 4 部品の共通語彙、正は message)。data-variant(solid/outline/plain)は shadcn の 7 色調を「塗り・枠線・無装飾」の 3 形態へ縮約(色調軸は pre-styled-ui の ColorPalette へ委譲)。data-group-position(single/first/middle/last)は連続発言の角丸連結用の表示状態のみで算出は利用者責務。reactions/reaction は role="group" + 任意 aria-label(role="img" は不採用)、reaction は data-selected 存在属性のみを持つ非インタラクティブなパーツ。collapse-trigger/collapse-content は collapsible の属性契約(OpenState/aria-expanded/aria-controls)を bubble scope のまま再利用(wasm-full の折りたたみクリック配線は未整備)。参照実体は shadcn/ui のみ(参照軸 #2001) | |
| Attachment(Primitives、イシュー #2111。Themes ページは #2112 で追加) | attachment | Root/Media/Content/Name/Meta/Progress/Actions/Action | なし(自由関数のみ、状態機械を持たない静的部品)。イシュータイトルの 7 パーツに加え、削除等の個別アクションを担う Action(button + 任意 aria-label + ネイティブ disabled)を追加(attachment.rs「イシュータイトルとの差分」節)。root へ data-variant(file/image)・data-state(idle/uploading/error)・data-disabled(存在属性)を付与。会話系 4 部品の共通語彙(data-role/data-align)は意図的に持たない(attachment.rs「会話系 4 部品の共通語彙への不追随」節)。progress は attachment scope の単純なスロットであり、呼び出し側が progress::Progress のパーツを入れ子にする契約(data-scope="progress" を内包しない)。name/meta は整形済み文字列を受け取るだけのスロット(数値・単位整形は行わない)。参照実体は shadcn/ui のみ(参照軸 #2001) | |
| Marker(Primitives、イシュー #2114。Themes ページは #2115 で追加) | marker | Root/Icon/Content | なし(自由関数のみ、状態機械を持たない静的部品)。root へ data-variant(note/divider/label)・data-tone(neutral/info/warning/danger、fandhe-frontend-pre-styled-ui の ColorPalette 同名 4 値の部分集合)を付与。会話系 4 部品の共通語彙(data-role/data-align)は意図的に持たない(marker.rs「会話系 4 部品の共通語彙への不追随」節)。divider/label variant の区切り線(水平線・左右の線)は本モジュールでは一切出力せず、fandhe-frontend-pre-styled-ui 側が separator パーツの再利用または CSS で描く契約(marker.rs「区切り線は headless で描かない」節)。icon は装飾スロットとして aria-hidden="true" を固定付与、root は role を固定付与しない。参照実体は shadcn/ui のみ(参照軸 #2001) | |
| HoverCard | hover_card | Root/Trigger/Positioner/Content/Arrow/ArrowTip | state::Disclosure。#1641 で参照突合済み: 是正なし、意図的差分(data-side/data-align は positioner へ透過〔Tooltip/Popover と同型〕、content tabindex="-1" 非付与〔wasm-full 未配線〕等) | |
| Carousel | carousel | Root/Control/PrevTrigger/NextTrigger/ItemGroup/Item/IndicatorGroup/Indicator | 独自実装(0..slide_count を循環し得る index 値、Disclosure/SingleSelect の語彙に収まらないため Component/Hydrate を直接実装。dispatch は "next"/"prev"/"goto"/"first"/"last"、Goto の範囲外 index は no-op で fail-closed。item は role="group" + aria-roledescription="slide" + 位置ラベル、indicator は aria-current。zag.js との参照突合(イシュー #1660)により data-orientation を全 8 パーツへ拡張し、item/indicator に data-index(0-origin)、item に data-inview を追加、Home/End キー相当の First/Last action を追加した。aria-hidden(非表示スライド)・aria-controls(trigger→item-group)・各パーツの id/dir・progress-text/autoplay-trigger パーツ・data-dragging/data-readonly は意図的に非追随(理由は crates/headless-ui/src/carousel.rs モジュール doc 参照)。autoplay(play/pause/aria-live 切替/delay)・キーボードイベントの DOM 配線は wasm-full 側の後続責務としてスコープ外) | |
| Drawer | drawer | Root/Trigger/Backdrop/Positioner/Content/Title/Description/CloseTrigger(Dialog と同一 8 パーツ、data-scope="drawer") | dialog::Dialog(Dialog の状態機械へ全委譲。新規状態機械は作らない。固有に持つのは画面端の方向を表す DrawerPlacement(start/end/top/bottom)を root/positioner/content へ data-placement として出力する処理のみ)。参照突合(イシュー #1639): content へ tabindex="-1" を固定付与(zag drawer.connect.ts/WAI-ARIA dialog パターンの前提)。fandhe-frontend-wasm-full は drawer scope を MAPPING_TABLE/OverlayKind::from_scope/focus_trap::should_trap のいずれにも配線しておらず、ハイドレーション後の Drawer は現状 inert(未配線は別イシューで追跡)。ark-ui の grabber/swipe-area 等のドラッグ操作パーツ・関連 data-* は意図的に非採用。イシュー #2193 で dialog と対称の close_trigger_with_variant を追加したが、上記のとおり drawer scope 自体が未配線のため footer 相当のアクション行に text variant を置いても現状 inert | |
| Link | link | Root | なし(自由関数のみ。external オプトインで target="_blank" + rel="noopener noreferrer" を不可分に付与。現在位置は aria-current="page" + data-current)。#1649 で参考サイト(chakra-ui / Radix Themes)と突合済み。差分なし(意図的差分: external の不可分付与、asChild/装飾 prop は Themes 責務) | |
| LinkOverlay | link_overlay | Root/Overlay | なし(自由関数のみ。::before 疑似要素の代わりに overlay 自身を styled 層で position: absolute; inset: 0; 展開するカード全面クリック化)。#1650 で参考サイト(chakra-ui LinkBox/LinkOverlay。ark-ui は該当ページ 404 で対応部品なし、Radix Primitives/Radix Themes にも対応部品なし)と突合済み。anatomy/data-*/ARIA 差分なし。是正: overlay の呼び出し側 attrs からの href 予約キー除去。意図的差分: external(旧 isExternal)/asChild 非提供、内側リンクの前面化は利用者側 CSS の責務 | |
| NavList | nav_list | Root/Heading/List/Item/Link | なし(自由関数のみ。role を一切付与しない文書ナビ専用部品。docs-site::nav.rs::sidebar を本部品へ移行済み。イシュー #1653 で参考サイト突合済み: ark-ui / Radix Primitives / Radix Themes に対応物なし・chakra-ui List〔真の対応物は Themes 層 list〕とは意味論が異なるため anatomy / data-* / ARIA の増減なし。root/link へ予約キーなりすまし除去(drop_reserved)を追加) | |
| ActionBar | action_bar | Root/Positioner/Content/SelectionTrigger/Separator/CloseTrigger | state::Disclosure(構造上最も近い先行例は Dialog。content は role="dialog"(非モーダル) + aria-label + 開状態のみの data-expanded + tabindex="-1"、close_trigger は children が空のとき(可視テキストを持たないアイコンのみボタン)に限り既定 aria-label="close" を付与し、可視テキストがある場合は付与しない(WCAG 2.5.3 Label in Name 対策)、separator は role="separator" + aria-orientation="vertical"。選択件数から open を導出する糖衣 API は持たず、開閉は呼び出し側が dispatch で制御する。イシュー #1647 で chakra-ui(実体は Ark Popover の再利用)と突合済み — role を "toolbar" から是正した破壊的変更を含む) | |
| Toast | toast | Group/Root/Title/Description/ActionTrigger/CloseTrigger | 独自実装(複数通知の有界キュー、max 超過時に最古を押し出す。Disclosure/SingleSelect の語彙に収まらないため Component/Hydrate を直接実装。aria-live は ToastStatus から決定的に導出(Error のみ "assertive")。タイマー自動 dismiss・"push" の文字列 dispatch は wasm-full 後続イシューのスコープ外。#1643 で Zag.js/ark-ui/Radix Primitives/chakra-ui と参照突合済み: root へ data-state="open"(固定値のみ)/tabindex="0"、group へ tabindex="-1" を追加し Toaster::view の group ラベル既定値 DEFAULT_GROUP_LABEL を新設。意図的差分としてスタック/スワイプ系 data-* 不採用・aria-live は root 配置維持・close-trigger 既定 aria-label 非付与・action-trigger data-disabled 非発行を確定) | |
| Checkbox | checkbox | Root/Control/Indicator/Label/HiddenInput | 独自実装("checked"/"unchecked"/"indeterminate" の 3 値、Switch と同じ理由で Component/Hydrate を直接実装。hidden-input がネイティブ <input type="checkbox"> を担い、フォーム送信・ブラウザネイティブ操作との互換を保つ) | |
| Checkbox Group | checkbox_group | Root/Label/Item/ItemControl/ItemIndicator/ItemText | state::MultiSelect を埋め込んだ複数選択状態機械(radio_group::RadioGroup と対称の薄いラッパー、新規状態機械の追加ではない)。単一選択版 radio_group と異なり dispatch は "select"/"deselect"/"toggle" の 3 語彙を受理する(WAI-ARIA checkbox パターンには選択解除ジェスチャが実在するため)。ネイティブ <input type="checkbox"> は自前パーツを持たず checkbox::hidden_input の入れ子再利用で賄う。CheckboxGroupProps(disabled/readonly/invalid)を root と item 系 4 パーツへ一律注入し data-disabled/data-readonly/data-invalid を出力する(ark-ui Checkbox.Group props 相当、イシュー #1603)。role="group" は WAI-ARIA 1.2 の aria-orientation Used in Roles に含まれないため aria-orientation は付与しない(data-orientation は維持、radio_group〔role="radiogroup"〕とは異なる) | |
| Progress(linear + circular) | progress | Root/Label/ValueText/Track/Range(linear)+ Circle/CircleTrack/CircleRange(circular、SVG。crates/headless-ui/src/progress.rs rustdoc の「Circular」節参照) | 独自実装(value(min..=max、または indeterminate を表す None)を持つ連続量の値状態機械。data-state("indeterminate"/"loading"/"complete")は Progress::data_state が一元管理し、パーツ関数間で分裂させない。circular の SVG ジオメトリ(--size/--thickness/--percent/stroke-dasharray/stroke-dashoffset)は CSS 変数参照の固定リテラルで表現する headless 中立設計。indeterminate 時は --percent 等を出力せず進捗系の値を捏造しない fail-closed 方針。参考サイト突合(イシュー #1633): label へ data-orientation(Track/Range と同じ語彙)、value_text へ aria-live="polite" を無条件付与する是正を実施。role="progressbar" は Radix Primitives に合わせ Root へ配置(Zag.js は Track へ置くが、既存の pre-styled-ui/wasm-full 契約を維持するため据え置き)。Root の --percent style・Range への data-value/data-max 複製・circular への data-orientation は意図的に非採用。キーボード操作は参照 4 サイトともなし) | |
| ToggleTip | toggle_tip | Root/Trigger/Positioner/Content/Arrow/ArrowTip | state::Disclosure。参考サイト突合(イシュー #1644): chakra-ui ToggleTip(Ark Popover 基盤)と突合し、是正なしと判定。ARIA 3 者境界(aria-haspopup/role="dialog" 非付与)は意図的差分として維持。fandhe-frontend-wasm-full に "toggle-tip" scope が未登録(headless::MAPPING_TABLE/overlay::OverlayKind/position::PositionedKind のいずれにも無い)のため click/Escape/外側クリック/placement は未配線 | |
| Toolbar | toolbar | Root/Button/Link/Separator/ToggleGroup/ToggleItem | 独自実装(roving tabindex を担う focused/item_count/loop_focus/orientation の複合状態機械、carousel::Carousel を雛形とする。Disclosure/SingleSelect の語彙に収まらないため Component/Hydrate を直接実装。押下状態の管理は独自実装せず toggle_group::ToggleGroup/toggle_group::MultiToggleGroup を再エクスポートして再利用する。separator は toolbar 自身の向きと直交する aria-orientation を出力し、link は link::root の reverse tabnabbing 対策へ完全委譲する。disabled 項目もフォーカス順序から除外しない(WAI-ARIA APG 推奨)。イシュー #1657 で Radix Primitives Toolbar と突合済み: button/link/toggle_group/toggle_item の自由関数へ先頭引数 orientation: Orientation を追加し data-orientation を出力する破壊的変更、separator は直交 data-orientation を追加、呼び出し側 attrs からの予約キー(role/aria-*/data-*/type/tabindex)なりすまし除去を drop_reserved へ統一。矢印キー・Home/End の実 DOM 配線は fandhe-frontend-wasm-full に現状存在せず未実装のまま(別 Issue 起票対象)) | |
| Menubar | menubar | Root/Menu/Trigger/Positioner/Content/Arrow/ArrowTip/Item/ItemText/ItemIndicator/ItemGroup/ItemGroupLabel/Separator/SubTrigger/SubContent/CheckboxItem/RadioItemGroup/RadioItem(18 パーツ。Arrow/ArrowTip/ItemText/ItemIndicator/CheckboxItem/RadioItemGroup/RadioItem はイシュー #1652 で Radix Primitives Menubar と参照突合し追加、11 → 18 パーツ) | 独自実装(roving tabindex + 単一開閉を担う focused/trigger_count/open/loop_focus/orientation の複合状態機械、toolbar::Toolbar を雛形とする。Disclosure/SingleSelect の語彙に収まらないため Component/Hydrate を直接実装。開いている Menu を跨いだ左右移動(Next/Prev/First/Last/Focus で open が新しい focused へ追随する)が Toolbar には無い固有の遷移規則。menu パーツは role="none" を固定付与し role="menubar" の子として menuitem/group 以外を挟まない。既存 menu mod の anatomy はそのまま再利用せず、state::OpenState・aria/data_attrs ヘルパと checked_data_state(CheckboxItem/RadioItem 用)のみを再利用する。サブメニューの開閉状態は呼び出し側が別途持つ menu::Menu インスタンスから SubTrigger/SubContent へ注入する。矢印キー・Home/End・typeahead の実 DOM 配線は fandhe-frontend-wasm-full に実装済み(keynav::wiring::handle_menubar_trigger_keydown/move_menubar_focus〔#1073〕。CheckboxItem/RadioItem の highlight 移動・checked トグル dispatch は未配線、イシュー #1652 で記録した既知ギャップ) | |
| Navigation Menu | navigation_menu | Root/List/Item/Trigger/ItemIndicator/Content/Link/Indicator | state::SingleSelect を埋め込んだ「高々 1 個の Trigger だけが開く」状態機械(accordion::Accordion と同型に委譲。独自の Component/Hydrate は実装しない)。NavigationMenuProps { orientation }(イシュー #1654、既定 Horizontal)を root/list/item/content へ通し data-orientation を出力(wasm-full の keynav.rs が矢印キー方向判定に読む)。item/content は value を data-value として出力し、item_indicator(aria-hidden="true" 固定)を新設。全パーツへ drop_reserved(呼び出し側 attrs からの固定キーなりすまし除去、A05 対策)を適用。role は一切付与しない(root は素の nav の暗黙 role(navigation)に依拠し、role="menu"/role="menuitem" は付与しない。nav_list と同じ判断)。アクティブリンクは aria-current="page" + data-current。イシュー #2187: ルートレベルの indicator(root 直下・list の兄弟、開いている Trigger の下でスライドするポインタ)を新設。aria-hidden="true" 固定、data-state(開いている項目が 1 つでもあれば open)・data-orientation・data-value(Some のときのみ)を出力し、style 属性は出力しない(着装は pre-styled-ui、実座標の計測は wasm-full の責務、§3.25 規則 2 に基づく 3 層分離)。viewport 寸法測定・data-motion・Viewport/Sub は docs/policy/intentional-non-adoption.md §3.25 規則 2 により本層へ持ち込まない | |
| VisuallyHidden | visually_hidden | Root | なし(自由関数のみ。視覚的には隠すが支援技術には読ませ続けるテキストコンテナ。aria-hidden を一切出力しない不変条件がある)。参考サイト突合(イシュー #1668): Radix Primitives・Radix Themes・chakra-ui の 3 参照サイトと突合し、anatomy(1 パーツ)・data-(非付与)・role/aria-(非付与)・キーボード操作(なし)ともに一致、是正なし。Ark UI は該当ページ 404。asChild/as(要素差し替え API)・視覚的に隠した input パートはいずれも意図的に非採用(checkbox/switch/radio_group/select の hidden input 系パーツが同用途を担う) | |
| SkipNav | skip_nav | Link/Content | なし(自由関数のみ。WCAG 2.1 SC 2.4.1 Bypass Blocks 対応の「本文へスキップ」リンク。link は呼び出し側から任意の URL を受け取らず常に #<id> のみを組み立てるためスキーム注入経路を構造的に持たない。参考サイト突合(イシュー #1663): chakra-ui と突合済みで anatomy/data-*/ARIA は増減なし。chakra-ui の SkipNavContent が出力する inline outline: 0 は docs/policy/intentional-non-adoption.md §3.25 規則 2 により非採用(Themes 版が CSS で担当)。Ark UI は該当ページ 404、Radix Primitives/Radix Themes にも該当部品なし) | |
| Clipboard | clipboard | Root/Label/Control/Input/Trigger/Indicator/ValueText | 独自実装(コピー済みかどうかの 2 値、Avatar/Switch と同じ理由で Component/Hydrate を直接実装。コピー済み表示は data-state 値語彙ではなく data-copied 存在属性で表現する ark-ui/chakra-ui の慣習に従う。コピー対象値(value)は状態機械に持たせず root の data-value 属性としてのみ出力する。navigator.clipboard.writeText 実配線・タイムアウトによる自動リセットは fandhe-frontend-wasm-full::headless_clipboard(writeText 成功ゲート・fail-closed・値ログ禁止)が提供する。イシュー #1631 で参考実装(ark-ui/Zag.js clipboard.connect.ts)と突合し、label に data-copied/for(input_id 引数)、input に data-readonly、trigger に既定 aria-label(TRIGGER_ARIA_LABEL_IDLE/TRIGGER_ARIA_LABEL_COPIED、呼び出し側 attrs で上書き可・fandhe-frontend-wasm-full がコピー成功/リセット時に既定値のときのみ反転)を追加した) | |
| QrCode | qr_code | Root/Frame(svg)/Pattern(path)/Overlay | なし(自由関数のみ。value/ecc から一意に導出される純粋な変換であり遷移可能な状態を持たない。外部依存ゼロの QR Model 2(ISO/IEC 18004)byte モードエンコーダ(qr_encode、非公開実装)を内蔵。QR 画像自体のダウンロード導線が必要な場合は download_trigger を組み合わせる。value の動的更新・numeric/alphanumeric/kanji モードはスコープ外。参考サイト突合(イシュー #1634): frame に xmlns を固定付与し、role="img" を aria_label 指定時のみ付与するよう是正(未指定時は WAI-ARIA 1.2 の Accessible Name Required 要件に合わせ role/aria-label とも付与しない)。呼び出し側 attrs によるフレームワーク固定属性(viewBox/xmlns/role/aria-label/d)のなりすましを dedup で除外。Root/Overlay の inline style・id・DownloadTrigger は意図的非採用(docs/policy/intentional-non-adoption.md §3.25 規則 2、data: URL 拒否)。キーボード操作なし) | |
| FloatingPanel | floating_panel | Root/Trigger/Positioner/Content/Header/Title/Control/StageTrigger/CloseTrigger/Body | state::Disclosure(開閉)+ 独自実装の Stage("default"/"minimized"/"maximized" の 3 値、Disclosure/SingleSelect の語彙に収まらないため steps::Steps/progress::Progress と同じ判断で本モジュール内の独自 enum とする)。座標は positioner の --fandhe-x/--fandhe-y(positioning::css_vars の CSS 変数名の語彙のみ再利用、placement 計算自体は行わずドラッグ操作によるビューポート絶対座標をそのまま反映)。content は role="dialog" を固定付与するが aria-modal は出力しない非モーダル overlay。dispatch は "open"/"close"/"toggle"/"minimize"/"maximize"/"restore"/"set_position"(payload "x,y" は有限 f64 としてパースできる場合のみ受理、NaN/inf・パース不能時は no-op)。参考サイト突合(イシュー #1640): ark-ui(zag floating-panel.connect.ts)の getHeaderProps/getControlProps との突合で header/control へ data-stage(現在の stage)を追加し、getBodyProps との突合で body へ Stage::Minimized 時の hidden 存在属性を追加した(header/control はシグネチャ変更〔破壊的〕、body は出力属性の追加のみ〔非破壊的〕)。dragTrigger/resizeTrigger anatomy・data-dragging/data-topmost/data-behind・CSS 変数 --width/--height/--z-index・content の tabIndex: 0 は実行時計測・未配線の矢印移動を理由に意図的に不採用(docs/policy/intentional-non-adoption.md §3.25 規則 2)。fandhe-frontend-wasm-full の headless 部品 → dispatch 対応表に floating-panel scope の行が無く、trigger/close-trigger/stage-trigger の click・Escape キー閉鎖・矢印キー移動は未配線(#1640 時点の事実)。ドラッグ移動・リサイズの実 DOM 配線・フォーカストラップ・topmost 管理は fandhe-frontend-wasm-full の将来イシューのスコープ外 | |
| ScrollArea | scroll_area | Root/Viewport/Content/Scrollbar/Thumb/Corner | なし(自由関数のみ。viewport に tabindex="0" を固定付与、scrollbar/thumb は data-orientation、scrollbar/corner は aria-hidden="true" を固定付与。JS によるスクロール位置追従・thumb drag は初期実装対象外。#1662 で参考サイト突合済み: drop_reserved による予約キー除去を追加、anatomy/data-*/ARIA の増減なし。参照側の計測・ポインタ由来 data-*(data-overflow-*/data-at-*/data-state 等)は docs/policy/intentional-non-adoption.md §3.25 規則 2 により非採用) | |
| DownloadTrigger | download_trigger | Root | なし(自由関数のみ。a[download] 属性による宣言的ダウンロードトリガー(ark-ui/chakra-ui の Blob/非同期 data 前提の DownloadTrigger を静的部品として代替)。href の URL スキーム検証は render() 側の既定経路(data:/blob: を含め deny-by-default)に委譲し、独自検証を追加しない。#1628 で参考サイトと突合済み、要素種別差・Space 非起動は意図的) | |
| Splitter | splitter | Root/Panel/ResizeTrigger/ResizeTriggerIndicator | 独自実装(各パネルの size/min/max(%)を fail-closed に正規化するパネルサイズ状態機械。Disclosure/SingleSelect の語彙に収まらないため Component/Hydrate を直接実装。resize-trigger は role="separator" + aria-valuemin/max/now(先行パネルのサイズ%)+ aria-orientation(セパレータ自体の向き、パネルレイアウトの向きとは逆)+ aria-controls(隣接 2 パネルの id)+ data-id("<leading>:<trailing>")を出力する WAI-ARIA Window Splitter パターン準拠。panel は data-index/data-id を出力する。参照突合(イシュー #1664): ark-ui docs/zag.js splitter.connect.ts/WAI-ARIA APG と突合し、panel(index: usize 引数追加)・resize_trigger(controls: &str を leading_id, trailing_id へ置換)のシグネチャ変更(破壊的)、SplitterAction::IncrementLarge/DecrementLarge(zag.js keyboardResizeBy 既定値 ×10 相当の状態機械のみ)、drop_reserved による予約キーなりすまし除去を追加した。Arrow/Home/End キーの DOM 配線はイシュー #1074 で fandhe-frontend-wasm-full が実装済み、Shift+Arrow・F6(フォーカス循環)・Enter(collapse/expand)は未実装のまま(別 Issue 起票対象)。data-focus/data-dragging・aria-orientation の zag.js 非反転出力は意図的に非採用) | |
| JsonTreeView | json_tree_view | Key/Colon/Value(tree_view の Root/Label/Tree/Branch/BranchControl/BranchIndicator/BranchContent/BranchIndentGuide/Item/ItemIndicator を構造部として再利用。Key/Colon/Value は BranchText/ItemText の内側へ入れ子で描画する) | tree_view::TreeView をそのまま再利用(新規状態機械なし)。決定的な JSON 風データ構造 JsonValue(外部依存ゼロの自前 enum、Object は挿入順保持の Vec ペア列)をツリー表示する。ノード識別子(data-value)は RFC 6901 JSON Pointer で決定的に導出し、value パーツの data-kind("null"/"boolean"/"number"/"string"/"array"/"object")は JsonValue::kind の固定語彙のみを出力する。expanded_to_depth は ark-ui defaultExpandedDepth 相当の決定的初期展開ヘルパ。イシュー #1661 で ark-ui/zag 実装と突合し、colon パーツの新設・data-kind 語彙の "bool" → "boolean" への変更(破壊的変更)を行った | |
| ColorPicker | color_picker | Root/Label/Control/Trigger/Positioner/Content/Area/AreaBackground/AreaThumb/ChannelSlider(+Track/+Thumb)/ChannelInput/ValueText/HiddenInput | Hsv + alpha: u8 + state::Disclosure(開閉)を埋め込んだ独自実装。色領域・色相/アルファスライダーの見た目は canvas 非依存(CSS グラデーション + area_x_percent/area_y_percent/hue_percent/alpha_percent の導出整数割合のみ)。ColorPickerProps(disabled/readonly/invalid/required)を Root/Label/Control/Trigger/Area/AreaBackground/AreaThumb/ChannelInput へ一律付与し、Label のみ data-required を追加(イシュー #1604 参照突合)。ChannelSlider(+Track/+Thumb) は data-channel(Channel::as_str() 固定語彙)+ data-orientation(Orientation 引数、Thumb には aria-orientation も付与)。ChannelInput は data-channel="hex" 固定リテラル + readonly 属性 + aria-invalid。dispatch は "open"/"close"/"toggle"/"set_hex"(Color::parse_hex 検証)/"set_channel"(payload "<channel>:<value>"、固定語彙 + 範囲検証)/"increment"/"decrement"(payload は Channel 固定語彙、0..=Channel::max() へ clamp、ラップしない)。パート名体系改名(hue-slider* → channel-slider*)・ValueSwatch・SwatchGroup 系・format 切替・pointer ドラッグと Arrow/Home/End/Esc keydown の DOM 配線はスコープ外 | |
| FileUpload | file_upload | Root/Label/Dropzone/Trigger/ItemGroup/Item/ItemName/ItemSizeText/ItemDeleteTrigger/ClearTrigger/HiddenInput | 独自実装(ファイルメタデータFileUploadItem(name/size_bytes/mime_type、File オブジェクト自体は非保持)の受理済み一覧 + 直近拒否履歴、SingleSelect/MultiSelect/TagsInput の語彙に収まらないため Component/Hydrate を直接実装。AddFiles は型付き API 限定で文字列 dispatch では受理しない。実 File API 接触は fandhe-frontend-wasm-full の headless_file_upload.rs に隔離し(docs/policy/intentional-non-adoption.md §7 の保留解除)、ItemPreview/ItemPreviewImage(object URL プレビュー)はスコープ外。FileUploadProps(disabled/readonly/invalid/required)が各パーツへ状態束を一律付与し、ItemType(Accepted/Rejected 固定語彙)が item 系パーツへ data-type を付与する(参照突合、イシュー #1609)) | |
| DateInput | date_input | Root/Label/Control/SegmentGroup/Segment/HiddenInput | 独自実装(年/月/日セグメント + フォーカス位置を持つ値状態機械。Disclosure/SingleSelect の語彙に収まらないため Component/Hydrate を直接実装。暦計算は date の PlainDate::new/parse_iso/days_in_month へ委譲し、本モジュール自体は現在時刻を取得しない。各パーツは DateInputProps(disabled/readonly/invalid/focused、イシュー #1626 で DateSegmentFlags から置換)を共有し、focused は control/segment-group の data-focus にのみ反映される。各 segment は role="spinbutton" + aria-valuemin/max/now(未入力時は valuenow 省略)+ aria-label("Year"/"Month"/"Day")+ data-type(year/month/day)+ data-editable(常時)+ data-value(入力済み)/data-placeholder-shown(未入力)+ aria-readonly/aria-disabled(対応するフラグが true のとき)を出力する WAI-ARIA Spinbutton パターン準拠(ark-ui/zag.js date-input machine の Data Attributes 表と突合済み、イシュー #1626)。segment_group は role="group" を固定付与する。3 セグメント充足時のみ PlainDate::new で実在日付として検証し(2/30 等は value() が None を返す fail-closed 契約。セグメント値自体は破棄せず data-invalid で可視化する)、hydration も同じ契約(構造的範囲外・パース不能のみ拒否、実在しない日付はそのまま受理)。dispatch 語彙は increment/decrement(境界で wrap-around)・page-increment/page-decrement(PageUp/PageDown 相当、境界で clamp)・home/end・prev/next(矢印キーでのセグメント間移動相当、year↔month↔day、端で留まる)・backspace(値があれば消去、既に未入力なら前のセグメントへフォーカス移動)・focus/blur・set-segment/set/clear。date_input::segment_group は segment_group(segmented control)とは無関係の別 anatomy スコープ。granularity(hour/minute/second)・range 選択・locale 依存整形・キーボード操作の実 DOM 配線は wasm-full 後続イシューのスコープ外 | |
| Timer | timer | Root/Area/Item/ItemValue/ItemLabel/Separator/Control/ActionTrigger | 独自実装(idle/running/paused/completed の 4 値、Clipboard と同じ理由で Component/Hydrate を直接実装。countdown/start_ms/target_ms/interval_ms の設定値も状態機械へ持たせ hydration で往復させる。tick(経過ミリ秒)を TimerAction::Tick として外部から明示的に注入する決定的状態機械であり std::time/Instant 等の時計 API に一切依存しない。docs/design/component-coverage-map.md 保留解除(date-time 系)。実 tick 駆動(setInterval)は fandhe-frontend-wasm-full::headless_timer が提供する。イシュー #1632 で zag.js timer.connect.ts と突合済み: area の role="timer"/aria-atomic/aria-label・separator の aria-hidden・action_trigger の phase 引数による hidden 導出を是正し、TimerControl::Restart(zag.js validActions に実在する 5 値目。是正前の本実装は欠いていたため zag.js に合わせて追加)を追加) | |
| Tour | tour | Root/Backdrop/Spotlight/Positioner/Arrow/ArrowTip/Content/Title/Description/ProgressText/Control/CloseTrigger/ActionTrigger | 独自実装(Idle/Active { step }/Skipped/Completed の 4 値、Steps/Toast と同じ理由で Component/Hydrate を直接実装。content は role="dialog" + tabindex="-1" + aria-labelledby/aria-describedby、Active 時のみ現在ステップの id を data-step として出力(イシュー #1666)。progress-text は aria-live="polite"。control(イシュー #1666 で追加、ark-ui data-part="control" 相当)は ActionTrigger/CloseTrigger を並べるコンテナ。action_trigger は TourTriggerKind(Next/Prev/Skip/Complete/Custom)引数で data-type を出力し、Prev は dispatch が no-op になる境界でのみ disabled/data-disabled を付与する(イシュー #1666)。positioner は現在ステップの placement(positioning::Placement)から data-side/data-align を静的出力するのみで座標計算は行わない(ADR §4.1)。spotlight は target を data-target としてエスケープ済み出力するのみで DOM 解決は行わない。対象要素の実座標追従・スクロール/リサイズ再計算・target セレクタの実解決・クリック/キーボードの実配線は fandhe-frontend-wasm-full の後続イシューのスコープ外。docs/design/component-coverage-map.md 保留解除(装飾系)) | |
| AngleSlider | angle_slider | Root/Label/Control/Thumb/MarkerGroup/Marker/ValueText/HiddenInput | 独自実装(0..=359 の整数角度値状態機械。受理時に value % 360 で正規化し、step は 1..=359 へ clamp する。"set" は 0 起点の step グリッドへ最近傍スナップしてから正規化し、"increment"/"decrement" は非負整数の剰余演算のみでラップアラウンドする(浮動小数点不使用)。AngleSliderProps(disabled/readonly/invalid)を root/label/control/thumb で共有し、data-* を一律付与する。"home"/"end" dispatch(AngleSliderAction::SetToMin/SetToMax)で最小値/step グリッド上の最大値へ設定する契約を持つが、fandhe-frontend-wasm-full の DOM keydown 配線は REQ-11(WASM バンドルサイズ)予算逼迫のため Arrow キーのみ対応(イシュー #1601)。Component/Hydrate を直接実装し、hydration は範囲外 value/step を HydrateError で拒否する fail-closed 契約。ポインタ座標→角度変換(atan2)は fandhe-frontend-wasm-full 側の純粋関数へ隔離し、本モジュールはポインタ座標を一切扱わない) | |
| SignaturePad | signature_pad | Root/Label(label 要素)/Control/Segment(svg)/SegmentPath(ストロークごと、path)/Guide/ClearTrigger/HiddenInput | 独自実装(strokes: Vec<Stroke> + disabled/read_only を持つ値状態機械、Component/Hydrate を直接実装。canvas は一切使用せず、stroke_path_d が同一座標列から常に同一の SVG d 属性値を生成する決定的純粋関数(出力文字集合は M/L/数字/./,/-/空白に閉じる)。dispatch は "add-stroke"/"clear"/"undo"、点数上限(MAX_POINTS_PER_STROKE)・ストローク数上限(MAX_STROKES)超過は fail-closed で拒否する。ポインタイベントからの座標収集は fandhe-frontend-wasm-full::headless_signature_pad の責務。参照突合(イシュー #1620): label/guide は data-disabled を反映し、control は role="application"/aria-roledescription="signature pad" を常時付与、非 disabled 時のみ tabindex="0"、disabled 時のみ aria-disabled="true" を切り替える) | |
| ImageCropper | image_cropper | Root/Viewport/Image/Selection/Handle/Grid | 独自実装(crop 矩形 x/y/width/height(u32)の整数純粋状態機械。canvas・ポインタ座標・浮動小数点を一切扱わず、アスペクト比固定時は width 主導・height 従属の整数丸めで導出し、範囲外なら width を再クランプしてから再導出する fail-closed 契約。dispatch は "move"/"resize"([HandlePosition] の 8 方位)/"set"/"reset"、payload はクライアント由来の信頼できない入力として厳密パース + fail-closed で扱う。実画像切り出し(ピクセルデータ生成)はスコープ外。イシュー #1610 で参照実装〔ark-ui/zag.js〕へ突合: 各パーツ関数が ImageCropperProps { disabled, dragging } を受け取り data-disabled/data-dragging を出力、キーボード操作の受け口が handle から selection(focusable な role="slider" + aria-roledescription="2d slider" + aria-valuemin/aria-valuemax/aria-valuenow/aria-valuetext)へ移動、handle は role="presentation" + aria-hidden の非 focusable へ変更・data-handle-position を data-position へ改名、grid は GridAxis 経由の data-axis を追加。action_for_key(key, modifiers) はキーボード → アクションの対応表を純粋関数として提供(DOM keydown 配線は wasm-full 側の後続責務のまま)) | |
| Fieldset | fieldset | Root/Legend/HelperText/ErrorText | なし(field と同じ判断で disabled/invalid は呼び出し側が決める SSR 静的な props。ネイティブ <fieldset>/<legend> の disabled 伝播・アクセシブルネーム自動関連付けを前提とし、error_text は非該当時 hidden 存在属性を付与する fail-closed 描画(field::error_text と同型)。legend_with_variant(LegendVariant, &FieldsetProps<'_>, attrs, children) で legend パーツへ data-variant(legend/label)を語彙として出力する(イシュー #2214、shadcn/ui FieldLegend の variant prop 突合)。既存 legend(data-variant を出力しない)はバイト単位で不変) | |
| Input Group | input_group | Root/Addon/Text/Button | なし(field/fieldset と同じ判断で disabled/invalid は呼び出し側が決める SSR 静的な props。実際の <input>/<textarea> は本モジュールが出力せず field::input/field::textarea を呼び出し側が合成する契約。merge_field_props は disabled・invalid の双方を OR 伝播(「1 group = 1 control」の構造のため invalid も伝播する点が fieldset と異なる)。イシュー #2062、Themes ページは #2063) |
4z. combobox / listbox の ARIA 関連付けは呼び出し側責務
combobox::input/combobox::trigger の controls/activedescendant(aria-controls/aria-activedescendant)、listbox::content の labelledby(aria-labelledby)はいずれも Option の opt-in 引数であり、本クレートは値を強制しない(構造・アクセシビリティの anatomy は提供するが、呼び出し側が渡す値の正しさまでは型で保証しない)。
- combobox: popup(
content)を描画するインスタンスはcontrolsにcontentのidを渡すこと。ハイライト中の候補(itemのhighlighted引数)が存在する構成ではactivedescendantにその候補のidを渡すこと(ARIA 1.2 combobox パターン、フォーカスを保持するinput側に配線する。content側ではない)。 - listbox:
contentのアクセシブルネームはlabelledby(対応するlabelのid)または呼び出し側attrs経由のaria-labelのいずれかで必ず与えること。
リポジトリ内呼び出し(docs-site の Primitives/Themes 全ページ)はこの規約に準拠しており、crates/headless-ui/tests/combobox.rs・crates/headless-ui/tests/listbox.rs・crates/docs-site/tests/combobox_aria_association.rs が契約テストとして固定・回帰防止している。監査結果・型必須化を採らなかった判断根拠・再評価トリガーは ../internal/headless-ui-implementation-notes.md §S を参照。
4a0. 色変換コア(color)
color モジュールは anatomy を持たない純粋関数モジュールであり、上表の UI コンポーネント群とは性質が異なる(ブラウザ API 依存なし・wasm 境界隔離の対象外)。RGB / HSL / HSV / HEX の相互変換を、外部依存ゼロ・整数演算のみで提供する。fandhe-frontend-pre-styled-ui::color_swatch(ColorSwatch)と color_picker(ColorPicker)が本モジュールの型・変換関数を土台にする。
- 型:
Rgb { r, g, b }(全フィールド公開、u8全域が有効値)/Hsl・Hsv(h: u16(0..=359)・s/l/v: u8(0..=100)、フィールド非公開・new()の fallible コンストラクタのみ公開)/Color(RGBA canonical 表現、from_rgb/from_rgba/parse_hex/to_hex_string)/ColorError(OutOfRange/InvalidHex、Displayは静的文言のみ)。 - 変換関数:
Rgb::to_hsl/Rgb::to_hsv(順方向)・Hsl::to_rgb/Hsv::to_rgb(逆方向)。すべてf32/f64を使わずi64スケール整数演算で完結する。 - 丸め規則: 正の有理数の丸めは round half up(
(2*num + den) / (2*den))で固定する。無彩色(max == min)はs = 0, h = 0と定義する。詳細はcrates/headless-ui/src/color.rsモジュール doc 冒頭「丸め規則」参照。 - fail-closed 契約:
Color::parse_hexは#rgb/#rgba/#rrggbb/#rrggbbaaの 4 形式以外をすべてErr(ColorError::InvalidHex)にする(黙って補正しない)。Hsl::new/Hsv::newは範囲外を構築不能にする。Color::to_hex_string()の出力字母は常に#+ 小文字 16 進数字に閉じる(ColorSwatch が CSS カスタムプロパティ値としてそのまま使う契約の根拠)。
4a. 位置決め(anchor positioning)
Popover/Tooltip/Menu/Select の positioner/arrow/arrow_tip は Floating UI 相当の placement 計算を実装済みである(正の規範文書は docs/design/anchor-positioning-design.md。以下 ADR)。
4a.1 対象コンポーネントと anatomy
| コンポーネント | 対応パーツ | data-scope | arrow の有無 |
|---|---|---|---|
| Popover | Positioner/Arrow/ArrowTip | "popover" | あり |
| Tooltip | Positioner/Arrow/ArrowTip | "tooltip" | あり |
| Menu | Positioner/Arrow/ArrowTip | "menu" | あり |
| Select | Positioner のみ | "select" | なし |
| Combobox | Positioner のみ(data-scope="combobox" の anatomy は実装済み) | "combobox" | なし |
| HoverCard | Positioner/Arrow/ArrowTip | "hover-card" | あり |
| ToggleTip | Positioner/Arrow/ArrowTip | "toggle-tip" | あり |
Combobox の positioner は SSR 静的マークアップ(開閉状態の data-state/ hidden)のみ実装済みであり、crates/wasm-full/src/position.rs の PositionedKind への Combobox バリアント追加(実 DOM 計測・OPEN_POSITIONER_SELECTOR への組み込み)は後続イシューのスコープである(select/menu/popover/tooltip と同型の position 連携完了は未了)。HoverCard も同様に、positioner/arrow/arrow_tip anatomy とパーツ関数の attrs 透過は実装済みだが、PositionedKind::from_scope への "hover-card" 追加(実 DOM 計測対象化)は後続イシューのスコープである。
再計算対象の走査は開いている positioner のみに限定するセレクタ [data-part="positioner"][data-state="open"](crates/wasm-full/src/position.rs の OPEN_POSITIONER_SELECTOR)を使う。
4a.2 placement API(positioning モジュール、クレートルート再エクスポート)
crates/headless-ui/src/positioning.rs が外部依存ゼロの純粋関数として提供し、クレートルート(lib.rs)から次の型・関数を再エクスポートする: compute_position / css_vars_style / data_align / data_side / placement_attrs / Align / ArrowPosition / Placement / PositioningConfig / Rect / ResolvedPosition / Side / Size。CSS 変数名定数は positioning::css_vars(X/Y/REFERENCE_WIDTH/ ARROW_X/ARROW_Y)としてクレートルートとは別に公開される。
- [
Placement] はSide(top/bottom/left/right)×Align(start/center/end)の組み合わせで、12 placement 語彙(top/top-start/top-end/bottom/bottom-start/bottom-end/left/left-start/left-end/right/right-start/right-end)を型として一元化する。as_str()/from_str()は相互に逆写像であり、from_str()は未知の値に対しNoneを返す(fail-closed)。 data-*契約:data-side(top/bottom/left/right)・data-align(start/center/end)は flip 適用後の確定値の出力専用であり、再計算のたびに上書きされる CSS セレクタ用の属性である。- 希望 placement(flip 適用前)は別の永続化領域である
data-requested-side/data-requested-align属性に保持する(wasm 層のreposition_oneが初回のみ書き込む。data-side/data-alignを希望値の保持先に流用すると flip 後に希望値が失われるため分離した、詳細は ADR §4.4a)。 - SSR/SSG では位置計算そのものをスキップし、[
placement_attrs] によるdata-side/data-alignの静的出力とpre-styled-ui側の静的 CSS フォールバックで初期表示を描画する。
4a.3 位置計算 API(純粋関数・外部依存ゼロ・web-sys 非依存)
入力型:
- [
Rect](x/y/width/height): anchor(参照要素)の矩形。 - [
Size](width/height): floating 要素・viewport の寸法。 - [
PositioningConfig](placement/offset/flip/shift/same_width):Defaultはbottom-center・offset: 0.0・flip/shift有効・same_width: false。
compute_position(anchor: Rect, floating: Size, viewport: Size, config: &PositioningConfig, has_arrow: bool) -> ResolvedPosition:
config.placementで主軸・交差軸座標を計算する。flip(主軸の単純反転 1 候補のみ)が有効かつ主軸方向で viewport をはみ出す場合、反転後の座標で置き換える(反転後も収まらない場合は反転後の座標をそのまま採用する)。shift(交差軸方向の viewport 内クランプ)を適用する。has_arrowがtrueのときのみ arrow 座標([ArrowPosition]、floating 要素左上原点の相対座標)を計算する(Select は arrow を持たないため呼び出し側がfalseを渡す)。
異常入力(NaN/Infinity・負の幅高さ・viewport 寸法 0 等)は fail-closed: panic!/unwrap() を使わず、config.placement のまま座標 (0.0, 0.0)・arrow: None を返す。
出力型 [ResolvedPosition](x/y/確定 placement/Option<ArrowPosition>)。
4a.4 CSS 変数契約(--fandhe-*)
| 変数 | 内容 |
|---|---|
--fandhe-x | floating 要素の確定 x 座標(px) |
--fandhe-y | floating 要素の確定 y 座標(px) |
--fandhe-reference-width | anchor 幅(sameWidth 用、same_width 有効時のみ出力) |
--fandhe-arrow-x | arrow の x 座標(px、arrow を持つ場合のみ出力) |
--fandhe-arrow-y | arrow の y 座標(px、arrow を持つ場合のみ出力) |
css_vars_style(position: &ResolvedPosition, reference_width: f64, same_width: bool) -> String:
same_width == falseのときは--fandhe-reference-width自体を出力しない(PositioningConfig::same_widthをそのまま渡す契約。コンポーネント種別ごとの sameWidth 既定値が実行時挙動に反映される)。position.arrowがSomeのときのみ arrow 2 変数を出力する。- 出力は内部生成の数値書式(px)のみからなり、非有限値は最終防御線として
0.0へ丸める。 - 戻り値は
("style", &value)として既存のattrs: Vec<(&'a str, &'a str)>引数へ渡し、[fandhe_frontend_core::render] の既定エスケープ経由で出力する契約とする(§6 不変条件 7 と同一)。
コンポーネント別の sameWidth 既定(fandhe-frontend-wasm-full の PositionedKind::same_width_default): Menu/Select/Menubar は true、Popover/Tooltip/NavigationMenu は false(Menubar/NavigationMenu はイシュー #1182 で追加、判断根拠は docs/design/wasm-full-architecture.md §23.1)。
4a.5 計測注入・再計算(fandhe-frontend-wasm-full の position モジュール)
headless-ui は web-sys 非依存のまま維持し、実 DOM 計測(getBoundingClientRect・viewport 寸法)とスクロール/リサイズ契機の再計算は fandhe-frontend-wasm-full(position モジュール)が担う。再計算はスクロール・リサイズイベントを契機とした離散的な呼び出しであり、autoUpdate 相当の連続監視は非採用。
- 純粋ロジック層(native
cargo test可):PositionedKind(from_scope: 未知のdata-scope値はNoneの fail-closed /has_arrow: Popover/Tooltip/Menu のみtrue(許可リスト形式。Select/ Menubar/NavigationMenu はfalse) /same_width_default: 上記表)・parse_side_attr/parse_align_attr(属性欠落・未知値はbottom/centerへ fail-closed)・resolve_requested_placement・Measurement・resolve_position(kind, measurement, requested) -> RepositionResult(flip/shift 常時有効・offset0.0固定)。 - 配線層(
#[cfg(target_arch = "wasm32")]):reposition_all(document 全体の開いている positioner をOPEN_POSITIONER_SELECTORで走査)・reposition_within(root自身とその子孫に走査を限定する版、イシュー #2209)・PositionController(scroll/resize リスナー)。fandhe-frontend-wasm-fullのheadless::wire_headless_component(標準の headless 配線 API)は配線時・dispatch 成功後の 2 箇所でreposition_withinを自動的に呼び、配線時に一度だけensure_global_controllerで thread_local 単一のPositionControllerを遅延生成する(イシュー #2209、親 #2208。詳細はdocs/design/wasm-full-architecture.md§34)。利用者がPositionControllerを明示的に組み立てなくても popover/tooltip/menu のpositionerへ実座標が書き込まれる。 - DOM 属性値(
data-side/data-requested-side等)は改ざんされうるクライアント入力として扱い、fail-closed でパースする。
4a.7 data-positioned マーカー契約(ADR §4.4b)
fandhe-frontend-wasm-full の position::wiring::reposition_one は座標反映のたびに positioner へ data-positioned=""(値なしの存在マーカー)を書き込む。headless-ui 層(本モジュール)は SSR/SSG のいずれの出力経路でもこの属性を一切出力しない([placement_attrs] は data-side/ data-align の 2 属性のみを返す)。fandhe-frontend-pre-styled-ui(crates/pre-styled-ui/src/menu.rs/select.rs の recipe()。イシュー #2210 で popover.rs/tooltip.rs の positioner にも同型の state 規則を追加した)はこの非対称性を利用し、マーカーの有無で「SSR 静的フォールバック(position: absolute + ローカル座標系)」と「wasm 確定座標(position: fixed + viewport 座標系、--fandhe-x/--fandhe-y を transform: translate3d で消費)」を切り替える。マーカー不在(wasm 未稼働)では常に静的表示へ fail-closed に留まる。arrow(Popover/Tooltip/ Menu のみ、has_arrow() が Select を対象外とする、§4a.2)は --fandhe-arrow-x/--fandhe-arrow-y を変数フォールバックのみで消費し、マーカー切り替えを必要としない(イシュー #2210 で popover.rs/ tooltip.rs も arrow/arrow-tip を消費するようになったため、この一文は 3 部品共通の契約として読み替える)。
4c. 暦計算コア(date モジュール)
date-time 系コンポーネント(Calendar / DatePicker / DateInput / Timer)の先行前提として、fandhe_frontend_headless_ui::date モジュールを実装した。他コンポーネントと異なり anatomy パーツ・状態機械を持たない、非描画の純計算モジュールである。
4c.1 公開 API 一覧
| 型/関数 | 役割 |
|---|---|
PlainDate | 年月日のみの日付(proleptic Gregorian、年 0000〜9999)。フィールド非公開・PlainDate::new 経由の検証済み構築のみ |
PlainDate::new(year, month, day) | 検証付き構築(唯一の構築経路)。範囲外は DateError |
PlainDate::year/month/day | 各フィールドの読み出し |
PlainDate::day_of_week() | [Weekday] を返す |
PlainDate::add_days(delta) | delta 日後(負なら前)を返す。checked_add + 範囲ガードで overflow/範囲外は Err(DateError::OutOfRange) |
PlainDate::days_until(other) | other - self の日数差(符号あり) |
PlainDate::parse_iso(s) / FromStr | 厳密 YYYY-MM-DD(ゼロ埋め・ハイフン区切り固定)のみ受理 |
PlainDate::to_iso_string() / Display | ゼロ埋め YYYY-MM-DD を返す(ASCII 数字とハイフンのみ) |
Weekday | 月曜始まりの曜日(iso_number(): 月曜 1〜日曜 7、from_iso_number(): 逆変換) |
is_leap_year(year) | 4/100/400 規則によるうるう年判定 |
days_in_month(year, month) | 指定年月の日数(28〜31) |
MonthGrid / month_grid(year, month, week_start) | 当月 1 日を含む週の先頭から月末を含む週の末尾まで、前後月の日で埋めた週配列(Vec<[PlainDate; 7]>)を返す Calendar 描画向けグリッド |
DateError | InvalidDate/InvalidFormat/OutOfRange の fail-closed エラー |
4c.2 決定性・現在時刻非取得の契約
- 現在時刻を一切取得しない:
SystemTime・Instant・js_sys等の時刻取得 API を呼ばない。「今日」は常に呼び出し側(Calendar 等の上位コンポーネント)がPlainDateとして明示的に渡す設計であり、同一入力から常に同一出力を返す。この不変条件はcrates/headless-ui/tests/date.rs::date_module_never_reads_the_current_time(include_str!によるソース走査、コメント行を除く実コード行のみ検査)が恒久的に機械強制する。 - 外部依存ゼロ(REQ-3):
core/標準ライブラリのみで完結し、crates/headless-ui/Cargo.tomlに依存を追加しない。 - fail-closed: 不正な年月日・不正な文字列・範囲逸脱・オーバーフローはすべて
Err(DateError)を返し、panic!/unwrap()/expect()を使わない(ライブラリコードでの unwrap/panic 回避方針、.claude/rules/coding-rust.md)。 - HTML を一切組み立てない: 本モジュールは非描画の純計算モジュールであり
raw_html()・HTML 文字列組み立てを持たない。to_iso_string()の出力(ASCII 数字とハイフンのみ)を後続コンポーネントが描画する際は、fandhe-frontend-coreの既定エスケープ(REQ-1)を必ず経由する契約とする。 - 内部アルゴリズム: 年月日 ⇔ エポック日数(1970-01-01 を 0 とする)の変換に Howard Hinnant の
days_from_civil/civil_from_daysとして知られる純整数アルゴリズムを使う。曜日・加減算・日付差・月グリッドの全 API をこの単一の変換対に載せることで、往復変換の性質テストだけで土台の正しさを固定できる。
4d. Calendar / DatePicker
date-time 系コンポーネントのうち、暦計算コア(§4c)を利用する Calendar / DatePicker を実装した。
4d.1 Calendar(calendar モジュール)
| 項目 | 内容 |
|---|---|
| 部品ページ | Calendar |
| anatomy パーツ | Root/Heading/PrevTrigger/NextTrigger/Table/TableHeader/TableRow/TableHeadCell/TableBody/TableCell/DayTrigger の 11 パーツ |
| 状態機械 | Calendar(view_year/view_month/selected/today/min/max/week_start)。CalendarAction: PrevMonth/NextMonth/Select(PlainDate)/ClearSelection |
| dispatch 名 | "prev-month"/"next-month"/"select"(payload は ISO 8601 文字列)/"clear-selection" |
| 決定性契約 | 「今日」は Calendar::new の today 引数として呼び出し側が明示的に渡す。本モジュールは現在時刻 API を一切呼ばない(§4c と同じ契約、crates/headless-ui/tests/calendar.rs::calendar_module_never_reads_the_current_time が機械強制) |
| fail-closed | min > max は Calendar::new が Err(DateError::InvalidDate) を返す。範囲外選択・年 0000/9999 境界での月移動は状態を変更しない(無移動)。Calendar::weeks() が Err を返す極端な境界では table_body_from_grid が空の tbody を返す(panic しない) |
| ARIA | WAI-ARIA APG grid パターン(role="grid"/"row"/"columnheader"/"gridcell")。今日は aria-current="date"、選択日は aria-selected="true" |
4d.2 DatePicker(date_picker モジュール)
| 項目 | 内容 |
|---|---|
| 部品ページ | DatePicker |
| anatomy パーツ | Root/Label/Control/Input/Trigger/ClearTrigger/Positioner/Content の 8 パーツ |
| positioner/content の基盤 | crate::popover(state::Disclosure)と同一の開閉・配置基盤を再利用する。独自のオーバーレイ機構は持たない |
| 状態機械 | DatePicker(state::Disclosure + calendar::Calendar の合成)。DatePickerAction: Open/Close/Toggle/PrevMonth/NextMonth/Select(PlainDate)/ClearSelection |
| dispatch 名 | "open"/"close"/"toggle"/"prev-month"/"next-month"/"select"(payload は ISO 8601 文字列)/"clear-selection"。"select" は ark-ui の closeOnSelect 既定 true に準拠し popover を閉じる |
input パーツ | ネイティブ <input type="text">。value は PlainDate::to_iso_string() 由来の ISO 8601 表記のみを受け取る契約(DateInput との連携は行わない) |
| DateInput との責務境界 | 本コンポーネントはセグメント式 DateInput に依存せず、ISO 8601 値のネイティブ <input> だけで完結する |
DatePickerProps(イシュー #1627) | disabled/readonly/invalid/required の 4 bool。root/label/control/input/trigger/clear_trigger の 6 パーツへ data-disabled/data-invalid/data-readonly を一律付与し、data-required は label のみへ付与する。input は props.disabled/props.readonly/props.required をそれぞれネイティブ disabled/readonly/required 存在属性へも反映し、props.invalid のときのみ aria-invalid="true" を追加する |
label の for_(イシュー #1627) | id(content/trigger の labelledby と対)に加え、for_(ark htmlFor 準拠)で input の id とネイティブ label[for] 関連付けを成立させる |
| 参照突合の非追随(イシュー #1627) | ark-ui の View/ViewControl/PrevTrigger/NextTrigger/ViewTrigger/RangeText/Table 系/TableCellTrigger/MonthSelect/YearSelect/PresetTrigger/WeekNumber\*/ValueText はグリッド系が content へ合成する calendar モジュール(11 パーツ)に既に存在し、年月ビュー切替・プリセット・週番号は非追随を継続。data-view/data-placement も非追随。fandhe-frontend-wasm-full への date-picker scope 配線(trigger click・clear・Escape・外側クリック閉鎖)は未実装(別イシュー扱い) |
4e. Format ユーティリティ(format モジュール)
ark-ui format-byte/format-number/format-time/format-relative-time・chakra-ui i18n/format-byte/format-number 相当の機能を、JS の Intl API・LocaleProvider 等の JS ランタイム機構に依存せず実装した。他コンポーネントと異なりノードを返さず anatomy/状態機械を持たない String 純関数群である。
| 関数 | オプション型 | 概要 |
|---|---|---|
format_byte | FormatByteOptions(unit: ByteUnit/unit_system: UnitSystem/unit_display: UnitDisplay/maximum_fraction_digits) | バイト数を "1.45 kB" 等の単位付き文字列へ整形。10 進(1000 進)/2 進(1024 進)の基数系列を選択可能 |
format_number | FormatNumberOptions(style: NumberStyle/minimum_fraction_digits/maximum_fraction_digits/use_grouping/sign_display: SignDisplay) | 桁区切り・小数桁・符号・パーセント表示を伴う数値整形 |
format_time | FormatTimeOptions(with_seconds_always/always_show_hours) | 経過秒数を HH:MM:SS/MM:SS へ整形(ロケール非依存) |
format_relative_time | FormatRelativeTimeOptions(locale/style: UnitDisplay) | target/base(Unix 秒)2 値の差から相対時刻文字列を返す |
4e.1a Locale
Locale は #[non_exhaustive] enum で En(既定)・Ja の 2 種を持つ。format_byte/format_number/format_relative_time の各オプションに locale: Locale フィールドとして含み、呼び出し側が明示的に渡す値型のみで完結する。ark-ui utilities/locale.md・chakra-ui i18n/locale-provider.md の React LocaleProvider(Context/Provider)に相当する機構は意図的に非採用であり、グローバル既定ロケール・スレッドローカル・環境変数参照を一切持たない(ambient authority を作らない設計、docs/policy/intentional-non-adoption.md §3.23 参照)。
Locale::tag(&self) -> &'static str: BCP 47 言語タグ("en"/"ja")を返すLocale::from_tag(tag: &str) -> Option<Locale>: タグ文字列からLocaleを決定的に逆引きする。ASCII 小文字化 +-/_前の primary subtag 完全一致のみに対応し("en-US"→Locale::En等)、未知タグはNone(Accept-Language ヘッダ解析等のロケールネゴシエーションは呼び出し側の責務)
ja の出力例(unit_display: UnitDisplay::Long):
format_byte(1000.0, ja, Long) -> "1 キロバイト"
format_byte(1024.0*1024.0, ja, Binary, Long) -> "1 メビバイト"
format_relative_time(base - 3*86400, base, ja, Long) -> "3 日前"
format_relative_time(base + 3*86400, base, ja, Long) -> "3 日後"
format_relative_time(base, base, ja, _) -> "たった今"short/narrow の単位記号(kB/k 等)は SI 表記が国際共通のため en と同一値。相対時刻の narrow 形式は数字と単位語彙の間にスペースを挟まない(例: "3時間前")が、long/short 形式は半角スペースを挟む(例: "3 日前"。CLDR ja の実挙動に整合、Rust 実装の定数表 + テスト網羅表を正とする)。
4e.1 決定性・丸め規則の契約
- 現在時刻 API に依存しない:
format_relative_timeの基準時刻baseは必ず呼び出し側が明示的に渡す。std::time::SystemTime::now()等を本モジュールが呼ぶことはない(crate::timer/crate::dateと同型の「時刻を渡される」設計)。 - 丸め規則:
format_byte/format_numberの固定小数点丸めはformat!("{:.prec$}")(Rust 標準の 2 進表現に基づく最近接丸め)を正とする(rustdoc に明記)。 - 非有限値・境界値: NaN/±∞ は panic せず
"NaN"/"∞"/"-∞"を返す。i64::MIN/i64::MAXを含む全入力域でunwrap()/panic!を使わずunsigned_abs()/checked_sub()で決定的な出力を返す(A04 対策)。
5. 呼び出し規約(SSR / CSR 共通の前提)
- 各コンポーネントの anatomy パーツ(
root/trigger/content等)は状態を引数で受け取る純粋関数として実装されており、SSR は自由関数を直接呼ぶだけで静的マークアップを組み立てられる(状態機械(Accordion/Dialog/Switch等の型)を経由する必要はない)。 - CSR/hydration は各コンポーネントの状態機械型(
Accordion/Dialog等)を経由し、fandhe_frontend_interactive::Component/Hydrateの dispatch で状態遷移する。クリック/キーボード操作の実挙動は wasm 層(fandhe-frontend-wasm-client/-wasm-full)の責務であり、本クレートのスコープ外。 examples/headless-pre-styled-uiは自由関数のみを使う SSR 静的ショーケースの実例。
6. セキュリティ不変条件
- 属性名(
data-*/aria-*/type/role/hidden/disabled/id等)はすべて&'static strリテラルで固定されており、動的値が属性名スロットへ混入する経路はない。 - 動的値(
value/id/controls/labelled_by/呼び出し側attrs/childrenテキスト)はfandhe_frontend_core::renderの既定エスケープ(REQ-1)を必ず経由する。本クレート内でraw_html()は使用しない。 data-state値語彙("open"/"closed"/"checked"/"unchecked"等)は各状態モジュール(state/switch/avatar等)に一元化し、パーツ関数側で独自の値を作らない。- hydration 属性(
data-hydrate-*)はクライアント側で改ざんされうる入力として扱う。各状態機械のHydrate実装は既存の状態機械(Disclosure/SingleSelect)へ委譲することで、panic せずHydrateErrorを返す保証を継承する。 #。unsafeはクレート全体で使用しない。- 外部依存は
fandhe-frontend-core/fandhe-frontend-interactive(いずれも path)のみ(.claude/rules/coding-rust.md)。加えて本クレートはfandhe_frontend_core・fandhe_frontend_interactiveの両方をクレートそのものとして再エクスポートし、本クレート単独依存の利用者がComponent/Hydrate/dispatch/HydrateError/render_for_hydrationを含む hydration API まで到達できるようにしている(docs/api/ pre-styled-ui-api.md§3b・crates/headless-ui/tests/ interactive_reexport.rs参照)。 positioning::css_vars_style(position, reference_width, same_width)が返すstyle属性値は内部生成の数値書式(px)のみからなり、呼び出し側は必ず既存のattrs引数 → 上記 2 の既定エスケープを経由して出力する(same_width == falseのとき--fandhe-reference-widthは出力しない、docs/design/anchor-positioning-design.md§7)。password_inputはパスワード値そのものを一切扱わない。input/PasswordInputはvalueを出力・保持する API を持たず、状態機械は表示切替の bool(visible)のみをフィールドに持つ。パスワード値がDebug/Hydrateの出力・エラーメッセージ・ログのいずれにも現れる余地がない設計であり、crates/headless-ui/src/password_input.rsの inline testinput_never_outputs_value_attributeが回帰を固定する。イシュー #1614(ark-ui/zag/Radix 参照突合)でPasswordInputProps.readonly(inputにネイティブreadonly+ 6 パーツ全てにdata-readonly。visibility_triggerへはネイティブdisabledを合成しない)を追加し、inputにdata-state・autocapitalize="off"・spellcheck="false"を固定付与した。aria-pressed(zag のaria-expandedに非追随)・tabindex非付与(tab 順序に残す)は意図的な差分として維持する。- イシュー #1615(ark-ui 公式 Data Attributes / Keyboard Support 表・Radix
one-time-password-field参照突合)でpin_inputへPinInputProps(disabled/readonly/invalid/required)を新設し、root/label/inputのdata-invalid/data-readonly(labelにのみ追加でdata-required)・inputのaria-invalid/ネイティブreadonly・data-index(0-origin 桁インデックス)/data-filled(値が非空のときのみ)を追加した。PinInputAction::Backspaceは「現在桁を消去し前の桁へ移動」へ是正し(旧実装は「消去して留まる」で ark-ui の Delete と区別が付かなかった)、PinInputAction::Delete(現在桁のみ消去、フォーカス移動なし)・PinInputAction::Prev/PinInputAction::Next(ArrowLeft/ ArrowRight)を新設した(dispatch 語彙:"delete"/"prev"/"next")。 formatモジュールはテキスト値を返す純関数であり、出力は呼び出し側が必ずfandhe_frontend_core::text()ノード → 上記 2 の既定エスケープを経由してから描画する(本モジュール自体は HTML を組み立てない)。std::time::SystemTime::now()等の現在時刻 API・環境変数・グローバル状態を一切参照しない決定的純関数であり、base/target等の時刻は必ず呼び出し側が明示的に渡す(§4e.1 参照)。- イシュー #1617(ark-ui Rating Group 参照突合)で
rating_groupへRatingGroupProps(disabled/readonly/required)を新設し、root/label/control/hidden_inputのシグネチャを&RatingGroupProps受けへ統一した。labelのdata-disabled/data-required、controlのdata-disabled/data-readonly/aria-required+ 真のときのみのaria-disabled="true"/aria-readonly="true"/aria-required="true"を追加した。当初案はRatingItemFlags::focusableによりitemへ rovingtabindexを追加していたが、対応する DOM 配線(fandhe-frontend-wasm-full)が無いまま「フォーカスは受けるが Arrow/Space/Enter のいずれも操作不能」な状態で公開してしまう codex-review 指摘を受け、本イシュー内で撤回した(itemはtabindexを一切出力しない)。data-half(allow_half)・aria-setsize/aria-posinset・aria-roledescription・aria-orientationは意図的に非採用のまま維持する(crates/headless-ui/src/rating_group.rsモジュール doc「参考サイト突合」節参照)。
7. 関連ドキュメント
docs/api/component-api.md:Node/el/text/raw_html/renderの凍結 API 表面(本クレートが薄く委譲する下層)docs/api/pre-styled-ui-api.md: 本クレートの上層(chakra-ui 相当)examples/headless-pre-styled-ui/README.md: 本クレートのショーケース正本サンプルdocs/design/anchor-positioning-design.md: anchor positioning の設計確定書(正の規範文書。docs サイト nav.toml 未登録の内部設計文書のためリンク化しない)docs/policy/intentional-non-adoption.md§3.20/§3.21: anchor positioning 関連(Floating UI 高度 middleware・CSS Anchor Positioning)の非採用判断の正(同様に nav.toml 未登録のためリンク化しない).claude/skills/ark-ui/: 設計時の参考にした ark-ui リファレンススキルdocs/internal/headless-ui-implementation-notes.md: 実装経緯・ロードマップ・トレーサビリティの記録(docs サイト非掲載のためリンク化しない)