Skip to content

Commit 3f7b59e

Browse files
coladarciclaude
andcommitted
feat: add stacked layout to odyssey_toggle_button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 385715f commit 3f7b59e

4 files changed

Lines changed: 86 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2026-04-29]
9+
10+
### Added
11+
12+
- `odyssey_toggle_button` now accepts a `layout` attr (`:inline` default, `:stacked` for vertical radio-style list). In stacked mode, options render as a full-width vertical list — each row has a radio circle SVG (filled blue when selected, outlined when not) with the option name to the right and an optional description beneath it. Options support a `description` key for stacked mode.
13+
814
## [2026-04-27]
915

1016
### Added

demo/lib/demo_web/live/odyssey_showcase_live.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ defmodule DemoWeb.OdysseyShowcaseLive do
3232
|> assign(:status_selection, "Active")
3333
|> assign(:size_selection, "Medium")
3434
|> assign(:channel_selection, :email)
35+
|> assign(:stacked_selection, :pre_tax)
3536
|> assign(:standalone_select_item, nil)
3637
|> assign(:form_data, form_data)
3738
|> assign(:form, to_form(form_data, as: "form"))
@@ -96,6 +97,11 @@ defmodule DemoWeb.OdysseyShowcaseLive do
9697
{:noreply, socket}
9798
end
9899

100+
@impl true
101+
def handle_event("change_commission_type", %{"value" => option}, socket) do
102+
{:noreply, assign(socket, :stacked_selection, option)}
103+
end
104+
99105
@impl true
100106
def handle_event("validate", %{"form" => form_params}, socket) do
101107
socket =
@@ -326,6 +332,22 @@ defmodule DemoWeb.OdysseyShowcaseLive do
326332
tooltip="Select how often you want to receive reports"
327333
/>
328334
</div>
335+
336+
<div>
337+
<h3 class="text-lg font-medium mb-2">Stacked Layout</h3>
338+
<.odyssey_toggle_button
339+
layout={:stacked}
340+
options={[
341+
%{label: "Pre Tax Percentage Commission", value: :pre_tax, description: "Commission calculated before taxes are applied"},
342+
%{label: "After Tax Percentage Commission", value: :after_tax, description: "Commission calculated after taxes are applied"},
343+
%{label: "Flat Rate Commission", value: :flat_rate, description: "Fixed dollar amount per booking"},
344+
%{label: "Wholesale Pricing", value: :wholesale}
345+
]}
346+
selected={@stacked_selection}
347+
on_change="change_commission_type"
348+
/>
349+
<p class="text-sm text-gray-600 mt-4">Selected: <strong>{inspect(@stacked_selection)}</strong></p>
350+
</div>
329351
</div>
330352
</section>
331353

lib/peek_app_sdk/ui/odyssey/toggle_button.ex

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ defmodule PeekAppSDK.UI.Odyssey.ToggleButton do
5151
attr(:tooltip, :string, required: false, doc: "optional tooltip text to display next to the label")
5252
attr(:tooltip_location, :string, default: "right", doc: "tooltip position: top, bottom, left, right")
5353
attr(:disabled, :boolean, default: false, doc: "disable the toggle button")
54+
attr(:layout, :atom, default: :inline, doc: "layout style: :inline (default) or :stacked (vertical radio-style list)")
5455
attr(:phx_target, :any, required: false, doc: "optional phx-target for LiveComponent integration")
5556
attr(:rest, :global)
5657

@@ -75,6 +76,7 @@ defmodule PeekAppSDK.UI.Odyssey.ToggleButton do
7576
|> assign_new(:phx_target, fn -> nil end)
7677
|> assign_new(:label, fn -> nil end)
7778
|> assign_new(:tooltip, fn -> nil end)
79+
|> assign_new(:layout, fn -> :inline end)
7880

7981
~H"""
8082
<%= if @label do %>
@@ -100,6 +102,58 @@ defmodule PeekAppSDK.UI.Odyssey.ToggleButton do
100102
end
101103

102104
# Private function component for the button group to avoid duplication
105+
defp do_odyssey_toggle_button(%{layout: :stacked} = assigns) do
106+
assigns = assign_new(assigns, :disabled, fn -> false end)
107+
108+
~H"""
109+
<div class="flex flex-col space-y-4" role="group">
110+
<button
111+
:for={option <- @options}
112+
type="button"
113+
disabled={@disabled}
114+
value={to_string(option_value(option))}
115+
phx-click={
116+
if @disabled || to_string(@selected) == to_string(option_value(option)),
117+
do: nil,
118+
else: @on_change
119+
}
120+
phx-target={@phx_target}
121+
class={[
122+
"flex items-start gap-4 text-left w-full",
123+
@disabled && "opacity-50 cursor-not-allowed",
124+
!@disabled && to_string(@selected) != to_string(option_value(option)) && "cursor-pointer"
125+
]}
126+
{@rest}
127+
>
128+
<%= if to_string(@selected) == to_string(option_value(option)) do %>
129+
<svg class="mt-0.5 shrink-0" width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
130+
<rect x="0.5" y="0.5" width="27" height="27" rx="13.5" fill="#F2F3FA"/>
131+
<rect x="0.5" y="0.5" width="27" height="27" rx="13.5" stroke="#DADCE7"/>
132+
<path d="M14.0058 22C12.9047 22 11.8681 21.7917 10.8958 21.375C9.92361 20.9583 9.07292 20.3854 8.34375 19.6562C7.61458 18.9271 7.04167 18.0767 6.625 17.105C6.20833 16.1334 6 15.0952 6 13.9905C6 12.8857 6.20833 11.8507 6.625 10.8854C7.04167 9.92014 7.61458 9.07292 8.34375 8.34375C9.07292 7.61458 9.92332 7.04167 10.895 6.625C11.8666 6.20833 12.9048 6 14.0095 6C15.1143 6 16.1493 6.20833 17.1146 6.625C18.0799 7.04167 18.9271 7.61458 19.6562 8.34375C20.3854 9.07292 20.9583 9.92169 21.375 10.8901C21.7917 11.8585 22 12.8932 22 13.9943C22 15.0953 21.7917 16.1319 21.375 17.1042C20.9583 18.0764 20.3854 18.9271 19.6562 19.6562C18.9271 20.3854 18.0783 20.9583 17.1099 21.375C16.1415 21.7917 15.1068 22 14.0058 22Z" fill="#3957EA"/>
133+
</svg>
134+
<% else %>
135+
<svg class="mt-0.5 shrink-0" width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
136+
<rect x="0.5" y="0.5" width="27" height="27" rx="13.5" fill="#F2F3FA"/>
137+
<rect x="0.5" y="0.5" width="27" height="27" rx="13.5" stroke="#DADCE7"/>
138+
</svg>
139+
<% end %>
140+
<div class="flex flex-col">
141+
<span class={[
142+
"font-medium text-base",
143+
to_string(@selected) == to_string(option_value(option)) && "text-gray-900",
144+
to_string(@selected) != to_string(option_value(option)) && "text-gray-700"
145+
]}>
146+
{option_text(option)}
147+
</span>
148+
<span :if={option_description(option)} class="text-sm text-gray-500 mt-0.5">
149+
{option_description(option)}
150+
</span>
151+
</div>
152+
</button>
153+
</div>
154+
"""
155+
end
156+
103157
defp do_odyssey_toggle_button(assigns) do
104158
assigns = assign_new(assigns, :disabled, fn -> false end)
105159

@@ -148,4 +202,7 @@ defmodule PeekAppSDK.UI.Odyssey.ToggleButton do
148202

149203
def option_icon(%{icon: icon}), do: icon
150204
def option_icon(_option), do: nil
205+
206+
def option_description(%{description: description}), do: description
207+
def option_description(_option), do: nil
151208
end

lib/peek_app_sdk/ui/odyssey/toggle_button_input_component.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ defmodule PeekAppSDK.UI.Odyssey.ToggleButtonInputComponent do
7272
tooltip={assigns[:tooltip]}
7373
tooltip_location={assigns[:tooltip_location] || "right"}
7474
disabled={assigns[:disabled] || false}
75+
layout={assigns[:layout] || :inline}
7576
{@rest}
7677
/>
7778
</div>

0 commit comments

Comments
 (0)