diff --git a/src/bag_of_holding.gleam b/src/bag_of_holding.gleam index c7acf6e..8dc2116 100644 --- a/src/bag_of_holding.gleam +++ b/src/bag_of_holding.gleam @@ -1,8 +1,8 @@ import bag_of_holding/inventory.{type Inventory, Inventory} import bag_of_holding/storage.{type Storage, Storage} +import bag_of_holding/tabs.{tabs} import gleam/bool import gleam/dict -import gleam/dynamic/decode import gleam/int import gleam/list import gleam/result @@ -404,8 +404,8 @@ fn inventory_view( const inventory_body_tab_id = "body" -fn inventory_body_tab(inventory: Inventory) -> Tab(Message) { - Tab( +fn inventory_body_tab(inventory: Inventory) -> tabs.Tab(Message) { + tabs.Tab( id: inventory_body_tab_id, name: "Body", contents: element.fragment([html.text("body")]), @@ -414,8 +414,8 @@ fn inventory_body_tab(inventory: Inventory) -> Tab(Message) { const inventory_backpack_tab_id = "backpack" -fn inventory_backpack_tab(inventory: Inventory) -> Tab(Message) { - Tab( +fn inventory_backpack_tab(inventory: Inventory) -> tabs.Tab(Message) { + tabs.Tab( id: inventory_backpack_tab_id, name: "Backpack", contents: element.fragment([html.text("backpack")]), @@ -424,126 +424,14 @@ fn inventory_backpack_tab(inventory: Inventory) -> Tab(Message) { const inventory_pouch_tab_id = "pouch" -fn inventory_pouch_tab(inventory: Inventory) -> Tab(Message) { - Tab( +fn inventory_pouch_tab(inventory: Inventory) -> tabs.Tab(Message) { + tabs.Tab( id: inventory_pouch_tab_id, name: "Pouch", contents: element.fragment([html.text("pouch")]), ) } -type Tab(message) { - Tab(id: String, name: String, contents: Element(message)) -} - -// Based on -// (2026-08-03) -/// All tabs must have unique ids. -fn tabs( - tabs: List(Tab(message)), - selected_tab: String, - selected_message: fn(String) -> message, -) -> Element(message) { - let first_tab = list.first(tabs) - let last_tab = list.last(tabs) - let #(buttons, panels) = - tabs_loop( - tabs:, - buttons: list.new(), - panels: list.new(), - selected_tab:, - selected_message:, - prev_tab: Error(Nil), - first_tab:, - last_tab:, - ) - - html.div([attribute.class("tabs")], [ - html.div([attribute.role("tablist")], buttons), - ..panels - ]) -} - -fn tabs_loop( - tabs tabs: List(Tab(message)), - buttons buttons: List(Element(message)), - panels panels: List(Element(message)), - selected_tab selected_tab: String, - selected_message selected_message: fn(String) -> message, - prev_tab prev_tab: Result(Tab(message), Nil), - first_tab first_tab: Result(Tab(message), Nil), - last_tab last_tab: Result(Tab(message), Nil), -) { - case tabs { - [] -> #(list.reverse(buttons), list.reverse(panels)) - [tab, ..rest] -> { - let next_tab = list.first(rest) - - let selected = tab.id == selected_tab - let button = - html.button( - [ - attribute.id("tab-" <> tab.id), - attribute.role("tab"), - attribute.aria_selected(selected), - attribute.autofocus(selected), - attribute.tabindex(case selected { - True -> 0 - False -> -1 - }), - attribute.aria_controls("tabpanel-" <> tab.id), - event.on_click(selected_message(tab.id)), - event.advanced("keydown", { - use key <- decode.field("key", decode.string) - let selected_tab = case key { - "ArrowLeft" -> prev_tab - "ArrowRight" -> next_tab - "Home" -> first_tab - "End" -> last_tab - _ -> Error(Nil) - } - case selected_tab { - Ok(selected_tab) -> - decode.success(event.handler( - dispatch: selected_message(selected_tab.id), - prevent_default: True, - stop_propagation: True, - )) - Error(Nil) -> - decode.failure( - event.handler(selected_message(tab.id), False, False), - "", - ) - } - }), - ], - [html.text(tab.name)], - ) - let panel = - html.div( - [ - attribute.id("tabpanel-" <> tab.id), - attribute.role("tabpanel"), - attribute.tabindex(0), - attribute.aria_labelledby("tab-" <> tab.id), - attribute.classes([#("hidden", !selected)]), - ], - [tab.contents], - ) - tabs_loop( - tabs: rest, - buttons: [button, ..buttons], - panels: [panel, ..panels], - selected_tab:, - selected_message:, - prev_tab: Ok(tab), - first_tab:, - last_tab:, - ) - } - } -} - fn failed_to_load_view(error: storage.Error) -> Element(Message) { let error_message = case error { storage.CouldNotGetStorageManager -> "Could not get storage manager." diff --git a/src/bag_of_holding/tabs.gleam b/src/bag_of_holding/tabs.gleam new file mode 100644 index 0000000..6daa91a --- /dev/null +++ b/src/bag_of_holding/tabs.gleam @@ -0,0 +1,118 @@ +import gleam/dynamic/decode +import gleam/list +import lustre/attribute +import lustre/element.{type Element} +import lustre/element/html +import lustre/event + +pub type Tab(message) { + Tab(id: String, name: String, contents: Element(message)) +} + +// Based on +// (2026-08-03) +/// All tabs must have unique ids. +pub fn tabs( + tabs: List(Tab(message)), + selected_tab: String, + selected_message: fn(String) -> message, +) -> Element(message) { + let first_tab = list.first(tabs) + let last_tab = list.last(tabs) + let #(buttons, panels) = + tabs_loop( + tabs:, + buttons: list.new(), + panels: list.new(), + selected_tab:, + selected_message:, + prev_tab: Error(Nil), + first_tab:, + last_tab:, + ) + + html.div([attribute.class("tabs")], [ + html.div([attribute.role("tablist")], buttons), + ..panels + ]) +} + +fn tabs_loop( + tabs tabs: List(Tab(message)), + buttons buttons: List(Element(message)), + panels panels: List(Element(message)), + selected_tab selected_tab: String, + selected_message selected_message: fn(String) -> message, + prev_tab prev_tab: Result(Tab(message), Nil), + first_tab first_tab: Result(Tab(message), Nil), + last_tab last_tab: Result(Tab(message), Nil), +) { + case tabs { + [] -> #(list.reverse(buttons), list.reverse(panels)) + [tab, ..rest] -> { + let next_tab = list.first(rest) + + let selected = tab.id == selected_tab + let button = + html.button( + [ + attribute.id("tab-" <> tab.id), + attribute.role("tab"), + attribute.aria_selected(selected), + attribute.autofocus(selected), + attribute.tabindex(case selected { + True -> 0 + False -> -1 + }), + attribute.aria_controls("tabpanel-" <> tab.id), + event.on_click(selected_message(tab.id)), + event.advanced("keydown", { + use key <- decode.field("key", decode.string) + let selected_tab = case key { + "ArrowLeft" -> prev_tab + "ArrowRight" -> next_tab + "Home" -> first_tab + "End" -> last_tab + _ -> Error(Nil) + } + case selected_tab { + Ok(selected_tab) -> + decode.success(event.handler( + dispatch: selected_message(selected_tab.id), + prevent_default: True, + stop_propagation: True, + )) + Error(Nil) -> + decode.failure( + event.handler(selected_message(tab.id), False, False), + "", + ) + } + }), + ], + [html.text(tab.name)], + ) + let panel = + html.div( + [ + attribute.id("tabpanel-" <> tab.id), + attribute.role("tabpanel"), + attribute.tabindex(0), + attribute.aria_labelledby("tab-" <> tab.id), + attribute.classes([#("hidden", !selected)]), + ], + [tab.contents], + ) + tabs_loop( + tabs: rest, + buttons: [button, ..buttons], + panels: [panel, ..panels], + selected_tab:, + selected_message:, + prev_tab: Ok(tab), + first_tab:, + last_tab:, + ) + } + } +}