diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..01f51b3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.css] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e8cf0c..b12005a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2026-04-29] + +### Added + +- `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. +- Added `.ody-neutral-{0,50,100,200,250,300,350,400}` CSS helper classes for applying Odyssey neutral palette colors to text. +- Added complete set of `.ody-*` CSS classes covering H1–H3, P1–P3, Mobile Footer, and link variants across regular/medium/bold weights. + ## [2026-04-27] ### Added diff --git a/assets/components/core-components.css b/assets/components/core-components.css index a907966..ecc19c4 100644 --- a/assets/components/core-components.css +++ b/assets/components/core-components.css @@ -68,8 +68,20 @@ /* Override DaisyUI's disabled background colors */ /* For default values we have default colors. Note: ghost and link has not yet been implemented */ - .btn[disabled]:not(.btn-link, .btn-ghost, .btn-secondary, .btn-tertiary, .btn-danger), - .btn.disabled:not(.btn-link, .btn-ghost, .btn-secondary, .btn-tertiary, .btn-danger) { + .btn[disabled]:not( + .btn-link, + .btn-ghost, + .btn-secondary, + .btn-tertiary, + .btn-danger + ), + .btn.disabled:not( + .btn-link, + .btn-ghost, + .btn-secondary, + .btn-tertiary, + .btn-danger + ) { @apply bg-interaction-300 hover:bg-interaction-300; } @@ -171,24 +183,24 @@ table.table.table-zebra td { } /* Checkbox fieldset styling - only for labels containing checkboxes */ - .fieldset label:has(input[type='checkbox']) { + .fieldset label:has(input[type="checkbox"]) { @apply inline-flex items-center w-fit text-neutrals-300 hover:text-neutrals-400 focus:text-neutrals-400; } - .fieldset label:has(input[type='checkbox']) input[type='checkbox'] { + .fieldset label:has(input[type="checkbox"]) input[type="checkbox"] { @apply flex-shrink-0; } - .fieldset label:has(input[type='checkbox']) span.label { + .fieldset label:has(input[type="checkbox"]) span.label { @apply text-base; position: relative; padding-left: 1.5rem; /* Space for checkbox */ } .fieldset - label:has(input[type='checkbox']) + label:has(input[type="checkbox"]) span.label - input[type='checkbox'] { + input[type="checkbox"] { position: absolute; left: 0; top: 50%; @@ -249,3 +261,322 @@ table.table.table-zebra td { .toast .alert span.hero-exclamation-circle { display: none !important; } + +/* Odyssey Neutral Text Colors */ + +.ody-neutral-0 { + color: #ffffff; +} +.ody-neutral-50 { + color: #fafaff; +} +.ody-neutral-100 { + color: #f2f3fa; +} +.ody-neutral-200 { + color: #dadce7; +} +.ody-neutral-250 { + color: #9ca1b9; +} +.ody-neutral-300 { + color: #656a81; +} +.ody-neutral-350 { + color: #414159; +} +.ody-neutral-400 { + color: #202024; +} + +/* Odyssey Text Styles */ + +.ody-p1 { + font-family: "IBM Plex Sans"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} + +.ody-p1-medium { + font-family: "IBM Plex Sans"; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 150%; /* 24px */ +} + +.ody-p1-bold { + font-family: "IBM Plex Sans"; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; /* 24px */ +} + +.ody-h3-medium { + font-family: "IBM Plex Sans"; + font-size: 20px; + font-style: normal; + font-weight: 500; + line-height: 125%; /* 25px */ +} + +/* Typography/H1/Regular */ +.ody-h1 { + font-family: "IBM Plex Sans"; + font-size: 28px; + font-style: normal; + font-weight: 400; + line-height: 125%; +} + +/* Typography/H1/Medium */ +.ody-h1-medium { + font-family: "IBM Plex Sans"; + font-size: 28px; + font-style: normal; + font-weight: 500; + line-height: 125%; +} + +/* Typography/H1/Bold */ +.ody-h1-bold { + font-family: "IBM Plex Sans"; + font-size: 28px; + font-style: normal; + font-weight: 700; + line-height: 125%; +} + +/* Typography/H2/Regular */ +.ody-h2 { + font-family: "IBM Plex Sans"; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 125%; +} + +/* Typography/H2/Medium */ +.ody-h2-medium { + font-family: "IBM Plex Sans"; + font-size: 24px; + font-style: normal; + font-weight: 500; + line-height: 125%; +} + +/* Typography/H2/Bold */ +.ody-h2-bold { + font-family: "IBM Plex Sans"; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 125%; +} + +/* Typography/H3/Regular */ +.ody-h3 { + font-family: "IBM Plex Sans"; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: 125%; +} + +/* Typography/H3/Bold */ +.ody-h3-bold { + font-family: "IBM Plex Sans"; + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: 125%; +} + +/* Typography/P2 Small/Regular */ +.ody-p2 { + font-family: "IBM Plex Sans"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; +} + +/* Typography/P2 Small/Medium */ +.ody-p2-medium { + font-family: "IBM Plex Sans"; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 150%; +} + +/* Typography/P2 Small/Bold */ +.ody-p2-bold { + font-family: "IBM Plex Sans"; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; +} + +/* Typography/P3 X-Small/Regular */ +.ody-p3 { + font-family: "IBM Plex Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 125%; +} + +/* Typography/P3 X-Small/Medium */ +.ody-p3-medium { + font-family: "IBM Plex Sans"; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 125%; +} + +/* Typography/P3 X-Small/Bold */ +.ody-p3-bold { + font-family: "IBM Plex Sans"; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 125%; +} + +/* Typography/Mobile Footer/Regular */ +.ody-mobile-footer { + font-family: "IBM Plex Sans"; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 125%; +} + +/* Typography/Mobile Footer/Medium */ +.ody-mobile-footer-medium { + font-family: "IBM Plex Sans"; + font-size: 10px; + font-style: normal; + font-weight: 500; + line-height: 125%; +} + +/* Typography/Mobile Footer/Bold */ +.ody-mobile-footer-bold { + font-family: "IBM Plex Sans"; + font-size: 10px; + font-style: normal; + font-weight: 700; + line-height: 125%; +} + +/* Typography/P1 Base/Regular Link */ +.ody-p1-link { + font-family: "IBM Plex Sans"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; +} + +/* Typography/P1 Base/Medium Link */ +.ody-p1-medium-link { + font-family: "IBM Plex Sans"; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 150%; + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; +} + +/* Typography/P1 Base/Bold Link */ +.ody-p1-bold-link { + font-family: "IBM Plex Sans"; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 150%; + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; +} + +/* Typography/P2 Small/Regular Link */ +.ody-p2-link { + font-family: "IBM Plex Sans"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 150%; + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; +} + +/* Typography/P2 Small/Medium Link */ +.ody-p2-medium-link { + font-family: "IBM Plex Sans"; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 150%; + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; +} + +/* Typography/P2 Small/Bold Link */ +.ody-p2-bold-link { + font-family: "IBM Plex Sans"; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 150%; + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; +} + +/* Typography/P3 X-Small/Link */ +.ody-p3-link { + font-family: "IBM Plex Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 125%; + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; +} diff --git a/demo/lib/demo_web/live/odyssey_showcase_live.ex b/demo/lib/demo_web/live/odyssey_showcase_live.ex index 0e13fda..a8bf663 100644 --- a/demo/lib/demo_web/live/odyssey_showcase_live.ex +++ b/demo/lib/demo_web/live/odyssey_showcase_live.ex @@ -32,6 +32,7 @@ defmodule DemoWeb.OdysseyShowcaseLive do |> assign(:status_selection, "Active") |> assign(:size_selection, "Medium") |> assign(:channel_selection, :email) + |> assign(:stacked_selection, :pre_tax) |> assign(:standalone_select_item, nil) |> assign(:form_data, form_data) |> assign(:form, to_form(form_data, as: "form")) @@ -96,6 +97,11 @@ defmodule DemoWeb.OdysseyShowcaseLive do {:noreply, socket} end + @impl true + def handle_event("change_commission_type", %{"value" => option}, socket) do + {:noreply, assign(socket, :stacked_selection, option)} + end + @impl true def handle_event("validate", %{"form" => form_params}, socket) do socket = @@ -326,6 +332,22 @@ defmodule DemoWeb.OdysseyShowcaseLive do tooltip="Select how often you want to receive reports" /> + +
+

Stacked Layout

+ <.odyssey_toggle_button + layout={:stacked} + options={[ + %{label: "Pre Tax Percentage Commission", value: :pre_tax, description: "Commission calculated before taxes are applied"}, + %{label: "After Tax Percentage Commission", value: :after_tax, description: "Commission calculated after taxes are applied"}, + %{label: "Flat Rate Commission", value: :flat_rate, description: "Fixed dollar amount per booking"}, + %{label: "Wholesale Pricing", value: :wholesale} + ]} + selected={@stacked_selection} + on_change="change_commission_type" + /> +

Selected: {inspect(@stacked_selection)}

+
diff --git a/lib/peek_app_sdk/ui/odyssey/back.ex b/lib/peek_app_sdk/ui/odyssey/back.ex index cea7482..f426f25 100644 --- a/lib/peek_app_sdk/ui/odyssey/back.ex +++ b/lib/peek_app_sdk/ui/odyssey/back.ex @@ -18,7 +18,7 @@ defmodule PeekAppSDK.UI.Odyssey.Back do def odyssey_back(assigns) do ~H""" - <.link navigate={@navigate} class="text-xl leading-6 text-zinc-900 hover:text-gray-primary no-underline"> + <.link navigate={@navigate} class="ody-h3-medium ody-neutral-400 leading-6 hover:text-gray-primary no-underline"> <.odyssey_icon name="hero-arrow-left" class="text-brand h-6 w-6 mr-4" /> {render_slot(@inner_block)} diff --git a/lib/peek_app_sdk/ui/odyssey/toggle_button.ex b/lib/peek_app_sdk/ui/odyssey/toggle_button.ex index 8ca565c..edeef32 100644 --- a/lib/peek_app_sdk/ui/odyssey/toggle_button.ex +++ b/lib/peek_app_sdk/ui/odyssey/toggle_button.ex @@ -51,6 +51,7 @@ defmodule PeekAppSDK.UI.Odyssey.ToggleButton do attr(:tooltip, :string, required: false, doc: "optional tooltip text to display next to the label") attr(:tooltip_location, :string, default: "right", doc: "tooltip position: top, bottom, left, right") attr(:disabled, :boolean, default: false, doc: "disable the toggle button") + attr(:layout, :atom, default: :inline, doc: "layout style: :inline (default) or :stacked (vertical radio-style list)") attr(:phx_target, :any, required: false, doc: "optional phx-target for LiveComponent integration") attr(:rest, :global) @@ -75,31 +76,85 @@ defmodule PeekAppSDK.UI.Odyssey.ToggleButton do |> assign_new(:phx_target, fn -> nil end) |> assign_new(:label, fn -> nil end) |> assign_new(:tooltip, fn -> nil end) + |> assign_new(:layout, fn -> :inline end) ~H""" <%= if @label do %>
- - <.odyssey_tooltip :if={@tooltip} text={@tooltip} location={@tooltip_location}>{@label} - <%= if is_nil(@tooltip) do %> - {@label} - <% end %> + + {@label} + <.odyssey_tooltip :if={@tooltip} text={@tooltip} location={@tooltip_location} /> <.do_odyssey_toggle_button {assigns} />
<% else %> -
+
<.do_odyssey_toggle_button {assigns} /> <.odyssey_tooltip text={@tooltip} location={@tooltip_location} />
- <.do_odyssey_toggle_button :if={is_nil(@tooltip)} {assigns} /> + <.do_odyssey_toggle_button :if={is_nil(@tooltip) || @layout == :stacked} {assigns} /> <% end %> """ end # Private function component for the button group to avoid duplication + defp do_odyssey_toggle_button(%{layout: :stacked} = assigns) do + assigns = assign_new(assigns, :disabled, fn -> false end) + + ~H""" +
+ +
+ """ + end + defp do_odyssey_toggle_button(assigns) do assigns = assign_new(assigns, :disabled, fn -> false end) @@ -148,4 +203,7 @@ defmodule PeekAppSDK.UI.Odyssey.ToggleButton do def option_icon(%{icon: icon}), do: icon def option_icon(_option), do: nil + + def option_description(%{description: description}), do: description + def option_description(_option), do: nil end diff --git a/lib/peek_app_sdk/ui/odyssey/toggle_button_input_component.ex b/lib/peek_app_sdk/ui/odyssey/toggle_button_input_component.ex index a855cce..d6d09ad 100644 --- a/lib/peek_app_sdk/ui/odyssey/toggle_button_input_component.ex +++ b/lib/peek_app_sdk/ui/odyssey/toggle_button_input_component.ex @@ -72,6 +72,7 @@ defmodule PeekAppSDK.UI.Odyssey.ToggleButtonInputComponent do tooltip={assigns[:tooltip]} tooltip_location={assigns[:tooltip_location] || "right"} disabled={assigns[:disabled] || false} + layout={assigns[:layout] || :inline} {@rest} />
diff --git a/test/peek_app_sdk/ui/odyssey_test.exs b/test/peek_app_sdk/ui/odyssey_test.exs index 2004c9a..604f022 100644 --- a/test/peek_app_sdk/ui/odyssey_test.exs +++ b/test/peek_app_sdk/ui/odyssey_test.exs @@ -168,7 +168,7 @@ defmodule PeekAppSDK.UI.OdysseyTest do assert html =~ ".*Time Unit.*<\/span>/s + assert html =~ ~r/.*Time Unit.*<\/span>/s assert html =~ "class=\"inline-flex rounded-lg\"" end @@ -188,7 +188,7 @@ defmodule PeekAppSDK.UI.OdysseyTest do assert html =~ ".*Communication Channel.*<\/span>/s + assert html =~ ~r/.*Communication Channel.*<\/span>/s assert html =~ "Email" assert html =~ "Text Message" # Should have hidden input for form integration