Headless + styled UI components (WAI-ARIA APG patterns) for Luna UI
| Path | Role |
|---|---|
| src/ | Top-level components (one file per pattern) |
| src/aria/ | ARIA attribute primitives |
| src/headless/ | Logic-only state machines (no DOM styling) |
| src/styled/ | Default-styled wrappers over headless |
| src/apg/ | APG-pattern reference scaffolding |
// moon.mod.json
{
"deps": {
"mizchi/luna": "0.20.0",
"mizchi/luna_components": "0.20.0"
}
}src/components/
├── headless/ # ヘッドレスコンポーネント (mizchi/luna/components/headless)
│ ├── aria.mbt # ARIA属性ヘルパー
│ ├── helpers.mbt # イベントハンドラー等
│ └── switch.mbt # ヘッドレスSwitch
├── default/ # スタイル付きコンポーネント (mizchi/luna/components/default)
│ ├── switch.mbt # デフォルトSwitch
│ ├── theme.mbt # CSS抽出API
│ └── themes/ # CSSファイル
│ ├── switch.css
│ └── index.css
└── *.mbt # 既存コンポーネント (mizchi/luna/components)import mizchi/luna/components/headless as @headless
fn my_switch() -> @luna.Node[@js.Any] {
let on = @signal.signal(false)
let props = @headless.use_switch(on, aria_label="Enable feature")
@luna.h("button",
props.attrs + [
("class", @luna.attr_static("relative inline-flex h-6 w-11 rounded-full bg-gray-200")),
],
[
@luna.h("span", [
("class", @luna.attr_static("inline-block h-4 w-4 rounded-full bg-white")),
], []),
]
)
}import mizchi/luna/components/default as @default
fn my_switch() -> @luna.Node[@js.Any] {
let on = @signal.signal(true)
@default.switch(on, aria_label="Enable notifications", [@luna.text("Notifications")])
}<link rel="stylesheet" href="path/to/themes/index.css">// 全コンポーネントのCSS
let css = @default.generate_theme_css()
// 個別コンポーネント
let switch_css = @default.switch_css()
// CSS Variables 付き
let full_theme = @default.generate_full_theme():root {
--switch-track-off: #d1d5db;
--switch-track-on: #3b82f6;
--accent: #3b82f6;
}.switch[data-state="on"] .switch__track {
background: var(--switch-track-on);
}pub(all) struct BreadcrumbItem {
href : String
label : String
current : Bool
}pub(all) struct ComboboxOption {
id : String
label : String
disabled : Bool
}pub(all) struct ListboxOption {
value : String
label : String
disabled : Bool
}pub(all) enum SortDirection {
None
Ascending
Descending
}pub(all) struct TableColumn {
id : String
label : String
sortable : Bool
header_scope : String
}pub(all) struct ToolbarItemDef {
id : String
label : String
toggle : Bool
disabled : Bool
}fn[E] accordion(items : Array[AccordionItem[E]], expanded_index : Int, use_regions? : Bool) -> Node[E, String]fn[E] accordion_dyn(items : Array[AccordionItem[E]], expanded_index : Signal[Int], use_regions? : Bool) -> Node[E, String]fn[E] accordion_header(id : String, expanded : Bool, controls : String, level? : Int, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] accordion_header_computed(id : String, expanded : () -> Bool, controls : String, level? : Int, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] accordion_header_dyn(id : String, expanded : Signal[Bool], controls : String, level? : Int, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] accordion_item(id : String, heading : String, level? : Int, content : Array[Node[E, String]]) -> AccordionItem[E]fn[E] accordion_multi(items : Array[AccordionItem[E]], expanded_ids : Array[String], use_regions? : Bool) -> Node[E, String]alert([@luna.text("Your changes have been saved.")])
alert(type_=Error, [@luna.text("Invalid email address.")])fn[E] alert_dialog(aria_label? : String, aria_labelledby? : String, aria_describedby? : String, open : Bool, on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] alert_dialog_dyn(aria_label? : String, aria_labelledby? : String, aria_describedby? : String, open : Signal[Bool], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn arrow_direction(key : String) -> Intfn auto_id(prefix : String) -> Stringtest {
let id1 = @luna_components.auto_id("tab")
let id2 = @luna_components.auto_id("tab")
assert_true(id1 != id2)
}fn[E] breadcrumb(aria_label? : String, separator? : String, items : Array[BreadcrumbItem]) -> Node[E, String]fn[E] button(type_? : ButtonType, disabled? : Bool, aria_label? : String, aria_describedby? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]button([@luna.text("Save")])
button(disabled=true, [@luna.text("Disabled")])fn[E] checkbox(checked : CheckboxState, disabled? : Bool, aria_label? : String, aria_labelledby? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] checkbox_computed(checked : () -> CheckboxState, disabled? : Bool, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] checkbox_dyn(checked : Signal[CheckboxState], disabled? : Bool, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn clamp_index(current : Int, direction : Int, length : Int) -> Intfn[E] combobox_input(id : String, listbox_id : String, expanded : Bool, value : String, autocomplete? : AutocompleteType, aria_label? : String, aria_labelledby? : String, active_descendant? : String, on_input? : EventHandler[E], on_keydown? : EventHandler[E], on_focus? : EventHandler[E], on_blur? : EventHandler[E]) -> Node[E, String]fn[E] combobox_input_dyn(id : String, listbox_id : String, expanded : Signal[Bool], value : Signal[String], autocomplete? : AutocompleteType, aria_label? : String, active_descendant : Signal[String], on_input? : EventHandler[E], on_keydown? : EventHandler[E], on_focus? : EventHandler[E], on_blur? : EventHandler[E]) -> Node[E, String]fn combobox_interactive(id : String, options : Array[ComboboxOption], placeholder? : String, aria_label? : String, on_select : (ComboboxOption) -> Unit) -> Node[Any, String]fn[E] combobox_option_dyn(id : String, option_id : String, active_id : Signal[String], disabled? : Bool, on_click? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] combobox_option_el(id : String, selected : Bool, disabled? : Bool, on_click? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn combobox_select_only(id : String, options : Array[ComboboxOption], initial_value? : String, aria_label? : String, on_select : (ComboboxOption) -> Unit) -> Node[Any, String]fn[E] dialog(aria_label? : String, aria_labelledby? : String, aria_describedby? : String, open : Bool, on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]dialog(
aria_labelledby="dialog-title",
open=true,
[
h2(id="dialog-title", [text("Confirm")]),
p([text("Are you sure?")]),
button([text("Yes")]),
button([text("No")])
]
)fn[E] dialog_backdrop(on_click? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] dialog_dyn(aria_label? : String, aria_labelledby? : String, aria_describedby? : String, open : Signal[Bool], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] disclosure_button(id? : String, expanded : Bool, controls : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] disclosure_button_computed(id? : String, expanded : () -> Bool, controls : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] disclosure_button_dyn(id? : String, expanded : Signal[Bool], controls : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn focus_dialog(dialog_id : String) -> Unitfn focus_first_in_dialog(dialog_id : String) -> Unitfn is_horizontal_arrow(key : String) -> Boolfn is_toggle_key(key : String) -> Boolfn is_vertical_arrow(key : String) -> Boolfn[E] link(href : String, target? : LinkTarget, aria_label? : String, aria_describedby? : String, disabled? : Bool, children : Array[Node[E, String]]) -> Node[E, String]link("https://example.com", [@luna.text("Visit Example")])
link("/about", target=Blank, [@luna.text("About (new tab)")])fn[E] link_button(on_click : EventHandler[E], aria_label? : String, children : Array[Node[E, String]]) -> Node[E, String]fn[E] listbox(aria_label? : String, aria_labelledby? : String, multiselectable? : Bool, on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] listbox_from_options(options : Array[ListboxOption], selected_value : String, aria_label? : String) -> Node[E, String]fn[E] listbox_multi(options : Array[ListboxOption], selected_values : Array[String], aria_label? : String) -> Node[E, String]fn make_accordion_keydown_handler(header_ids : Array[String], current_index : Int, on_toggle? : () -> Unit) -> EventHandler[Any]fn make_combobox_handler(is_open : () -> Bool, filtered_options : () -> Array[ComboboxOption], current_index : () -> Int, open_listbox : () -> Unit, close_listbox : () -> Unit, set_index : (Int) -> Unit, on_select : (ComboboxOption) -> Unit) -> EventHandler[Any]fn make_horizontal_nav_handler(current_index : () -> Int, length : Int, on_change : (Int) -> Unit, wrap? : Bool, support_home_end? : Bool) -> EventHandler[Any]fn make_menu_button_handler(is_open : () -> Bool, open_menu : () -> Unit, close_menu : () -> Unit, focus_first : () -> Unit, focus_last : () -> Unit) -> EventHandler[Any]fn make_menu_nav_handler(current_index : () -> Int, item_count : Int, on_select : (Int) -> Unit, close_menu : () -> Unit, on_activate : (Int) -> Unit) -> EventHandler[Any]fn make_radio_keydown_handler(options : Array[RadioOption[Any]], radio_ids : Array[String], current_index : Int, on_select : (Int) -> Unit) -> EventHandler[Any]fn make_slider_drag_handler(min : Double, max : Double, step : Double, on_change : (Double) -> Unit) -> EventHandler[Any]fn make_slider_handler(current_value : () -> Double, min : Double, max : Double, step : Double, on_change : (Double) -> Unit, large_step? : Double) -> EventHandler[Any]fn make_sort_handler(column_id : String, current_sort_column : () -> String, current_sort_direction : () -> SortDirection, on_sort : (String, SortDirection) -> Unit) -> EventHandler[Any]fn make_spinbutton_handler(current_value : () -> Double, min : Double?, max : Double?, step : Double, on_change : (Double) -> Unit, large_step? : Double) -> EventHandler[Any]fn make_spinbutton_input_handler(min : Double?, max : Double?, on_change : (Double) -> Unit, fallback_value : () -> Double) -> EventHandler[Any]fn make_table_handler(row_count : Int, col_count : Int, current_row : () -> Int, current_col : () -> Int, on_change : (Int, Int) -> Unit, focus_cell : (Int, Int) -> Unit, page_size? : Int) -> EventHandler[Any]fn make_toolbar_handler(current_index : () -> Int, item_count : Int, on_change : (Int) -> Unit, orientation : ToolbarOrientation, focus_item : (Int) -> Unit) -> EventHandler[Any]fn make_vertical_nav_handler(current_index : () -> Int, length : Int, on_change : (Int) -> Unit, wrap? : Bool, support_home_end? : Bool) -> EventHandler[Any]fn[E] menu_button(popup_type : PopupType, expanded : Bool, controls? : String, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]menu_button(Menu, false, controls="dropdown", [@luna.text("Options")])fn[E] menu_button_dyn(popup_type : PopupType, expanded : Signal[Bool], controls? : String, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] menu_trigger(id : String, expanded : Bool, menu_id : String, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] menu_trigger_dyn(id : String, expanded : Signal[Bool], menu_id : String, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] menuitem(id? : String, selected : Bool, disabled? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] menuitem_dyn(id? : String, selected_index : Signal[Int], index : Int, disabled? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]meter(75.0, aria_label="Battery level", [])
meter(50.0, aria_valuetext="50% (6 hours) remaining", aria_label="Battery", [])fn[E] modal(aria_label? : String, aria_labelledby? : String, open : Bool, on_backdrop_click? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]nav(aria_label="Main navigation", [link("/", [text("Home")]), ...])page_layout(
header=[text("Site Name")],
nav=[link("/", [text("Home")])],
main_content=[text("Welcome")],
footer=[text("© 2024")]
)fn[E] radio(value : String, checked : Bool, disabled? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] radio_computed(value : String, checked : () -> Bool, disabled? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] radio_dyn(value : String, selected_value : Signal[String], disabled? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] radio_group(name : String, options : Array[RadioOption[E]], selected_value : String, aria_label? : String) -> Node[E, String]fn[E] radio_option(value : String, label : Array[Node[E, String]], disabled? : Bool) -> RadioOption[E]region(aria_label="Quick Stats", [text("...")])fn[E] slider(value : Double, min? : Double, max? : Double, step? : Double, orientation? : SliderOrientation, aria_label? : String, aria_labelledby? : String, aria_valuetext? : String, on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] slider_dyn(value : Signal[Double], min? : Double, max? : Double, orientation? : SliderOrientation, aria_label? : String, aria_valuetext_fn? : (Double) -> String, on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] slider_native(name : String, value : Double, min? : Double, max? : Double, step? : Double, aria_label? : String, id? : String) -> Node[E, String]fn[E] spinbutton(value : Double, min? : Double, max? : Double, step? : Double, aria_label? : String, aria_labelledby? : String, aria_valuetext? : String, disabled? : Bool, is_readonly? : Bool, on_keydown? : EventHandler[E], on_input? : EventHandler[E]) -> Node[E, String]fn[E] spinbutton_dyn(value : Signal[Double], min? : Double, max? : Double, aria_label? : String, aria_valuetext_fn? : (Double) -> String, on_keydown? : EventHandler[E], on_input? : EventHandler[E]) -> Node[E, String]fn[E] spinbutton_native(name : String, value : Double, min? : Double, max? : Double, step? : Double, aria_label? : String, id? : String) -> Node[E, String]fn[E] switch(on : Bool, disabled? : Bool, aria_label? : String, aria_labelledby? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] switch_computed(on : () -> Bool, disabled? : Bool, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] switch_dyn(on : Signal[Bool], disabled? : Bool, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] tab(id : String, selected : Bool, controls : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] tab_computed(id : String, selected : () -> Bool, controls : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] tab_dyn(id : String, selected : Signal[Bool], controls : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn table_interactive(id : String, columns : Array[TableColumn], rows : Array[TableRow], aria_label? : String, selectable? : Bool, on_row_select? : (String) -> Unit, on_sort? : (String, SortDirection) -> Unit) -> Node[Any, String]fn[E] tablist(aria_label? : String, orientation? : TabsOrientation, children : Array[Node[E, String]]) -> Node[E, String]fn[E] tabs(items : Array[TabItem[E]], selected_index : Int, aria_label? : String, orientation? : TabsOrientation) -> Node[E, String]fn[E] tabs_dyn(items : Array[TabItem[E]], selected_index : Signal[Int], aria_label? : String, orientation? : TabsOrientation) -> Node[E, String]fn[E] th(id? : String, scope? : String, sort? : SortDirection, on_click? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] th_dyn(id : String, scope : String, sort_column : Signal[String], sort_direction : Signal[SortDirection], on_click? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] toggle_button(pressed : ToggleState, disabled? : Bool, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]toggle_button(Pressed, [@luna.text("Mute")])fn[E] toggle_button_dyn(pressed : Signal[Bool], disabled? : Bool, aria_label? : String, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] toolbar(aria_label? : String, aria_labelledby? : String, orientation? : ToolbarOrientation, children : Array[Node[E, String]]) -> Node[E, String]fn toolbar_interactive(id : String, items : Array[ToolbarItemDef], pressed_states : Signal[Map[String, Bool]], orientation? : ToolbarOrientation, aria_label? : String, on_item_click : (String, Bool) -> Unit) -> Node[Any, String]fn[E] toolbar_item(selected : Bool, pressed? : Bool, disabled? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn toolbar_item_def(id : String, label : String, toggle? : Bool, disabled? : Bool) -> ToolbarItemDeffn[E] toolbar_item_dyn(focused_index : Signal[Int], index : Int, pressed? : Bool, disabled? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] tr(id? : String, selected? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] tr_dyn(id : String, selected_row : Signal[String], on_click? : EventHandler[E], on_keydown? : EventHandler[E], children : Array[Node[E, String]]) -> Node[E, String]fn[E] treeitem(id? : String, selected : Bool, expanded? : Bool, has_children : Bool, focused : Bool, disabled? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], label_content : Array[Node[E, String]], children? : Array[Node[E, String]]) -> Node[E, String]fn[E] treeitem_dyn(id? : String, selected : Signal[String], item_id : String, expanded? : Signal[Map[String, Bool]], has_children : Bool, focused_id : Signal[String], disabled? : Bool, on_click? : EventHandler[E], on_keydown? : EventHandler[E], label_content : Array[Node[E, String]], children? : Array[Node[E, String]]) -> Node[E, String]fn wrap_index(current : Int, direction : Int, length : Int) -> IntInstall
Download zipHeadless + styled UI components (WAI-ARIA APG patterns) for Luna UI