diff --git a/packages/live_ui/lib/live_ui/renderer.ex b/packages/live_ui/lib/live_ui/renderer.ex
index 14f6072e..b3340c01 100644
--- a/packages/live_ui/lib/live_ui/renderer.ex
+++ b/packages/live_ui/lib/live_ui/renderer.ex
@@ -104,6 +104,38 @@ defmodule LiveUi.Renderer do
<%= for child <- child_elements(@element, :default) do %>
<.render element={child} event_target={@event_target} />
<% end %>
+
+ <%= if get_in(@element.attributes, [:shell, :search_event]) do %>
+
+ <% end %>
+ <%= if get_in(@element.attributes, [:shell, :settings_event]) do %>
+
+ <% end %>
+ <%= if get_in(@element.attributes, [:shell, :user_avatar_url]) do %>
+
+ <% end %>
+
"""
end
diff --git a/packages/live_ui/test/live_ui/renderer_test.exs b/packages/live_ui/test/live_ui/renderer_test.exs
index b73fcf01..9e29050c 100644
--- a/packages/live_ui/test/live_ui/renderer_test.exs
+++ b/packages/live_ui/test/live_ui/renderer_test.exs
@@ -4,7 +4,7 @@ defmodule LiveUi.RendererTest do
import Phoenix.LiveViewTest
alias UnifiedIUR.{Container, Element, Forms, Interaction, Layout}
- alias UnifiedIUR.Widgets.{Foundational, Input, Navigation}
+ alias UnifiedIUR.Widgets.{Components, Foundational, Input, Navigation}
test "renderer maps foundational canonical widgets and layouts into native components" do
element =
@@ -251,4 +251,76 @@ defmodule LiveUi.RendererTest do
assert html =~ ~s(name="element_id" value="profile-name")
assert html =~ ~s(name="widget" value="text_input")
end
+
+ describe "top_strip trailing-region affordances" do
+ test "renders basic top_strip without trailing affordances" do
+ element = Components.top_strip([], brand: "Ariston", context: "Dashboard")
+ html = render_component(&LiveUi.Renderer.render/1, %{element: element})
+
+ assert html =~ ~s(data-live-ui-shell-position="top")
+ assert html =~ "live-ui-top-strip-brand"
+ assert html =~ "Ariston"
+ assert html =~ "live-ui-top-strip-trailing"
+ refute html =~ "live-ui-top-strip-search"
+ refute html =~ "live-ui-top-strip-settings"
+ refute html =~ "live-ui-top-strip-avatar"
+ end
+
+ test "renders search button when search_event is set" do
+ element = Components.top_strip([], brand: "Ariston", search_event: "open_search")
+ html = render_component(&LiveUi.Renderer.render/1, %{element: element})
+
+ assert html =~ "live-ui-top-strip-search"
+ assert html =~ ~s(aria-label="Open search")
+ assert html =~ ~s(phx-click="open_search")
+ refute html =~ "live-ui-top-strip-settings"
+ refute html =~ "live-ui-top-strip-avatar"
+ end
+
+ test "renders settings button when settings_event is set" do
+ element = Components.top_strip([], brand: "Ariston", settings_event: "open_settings")
+ html = render_component(&LiveUi.Renderer.render/1, %{element: element})
+
+ assert html =~ "live-ui-top-strip-settings"
+ assert html =~ ~s(aria-label="Open settings")
+ assert html =~ ~s(phx-click="open_settings")
+ refute html =~ "live-ui-top-strip-search"
+ refute html =~ "live-ui-top-strip-avatar"
+ end
+
+ test "renders avatar img when user_avatar_url is set" do
+ element =
+ Components.top_strip([],
+ brand: "Ariston",
+ user_avatar_url: "https://example.com/avatar.png"
+ )
+
+ html = render_component(&LiveUi.Renderer.render/1, %{element: element})
+
+ assert html =~ "live-ui-top-strip-avatar"
+ assert html =~ ~s(src="https://example.com/avatar.png")
+ assert html =~ ~s(aria-label="User profile")
+ refute html =~ "live-ui-top-strip-search"
+ refute html =~ "live-ui-top-strip-settings"
+ end
+
+ test "renders all three trailing affordances together" do
+ element =
+ Components.top_strip([],
+ brand: "Ariston",
+ search_event: "open_search",
+ settings_event: "open_settings",
+ user_avatar_url: "https://example.com/avatar.png"
+ )
+
+ html = render_component(&LiveUi.Renderer.render/1, %{element: element})
+
+ assert html =~ "live-ui-top-strip-search"
+ assert html =~ ~s(phx-click="open_search")
+ assert html =~ "live-ui-top-strip-settings"
+ assert html =~ ~s(phx-click="open_settings")
+ assert html =~ "live-ui-top-strip-avatar"
+ assert html =~ ~s(src="https://example.com/avatar.png")
+ end
+ end
end
diff --git a/packages/unified_iur/lib/unified_iur/widgets/components.ex b/packages/unified_iur/lib/unified_iur/widgets/components.ex
index 983a4910..c6b6c195 100644
--- a/packages/unified_iur/lib/unified_iur/widgets/components.ex
+++ b/packages/unified_iur/lib/unified_iur/widgets/components.ex
@@ -521,13 +521,17 @@ defmodule UnifiedIUR.Widgets.Components do
:top_strip,
:layer_shell_and_callout,
%{
- shell: %{
- position: :top,
- brand: option(opts, :brand, ""),
- context: option(opts, :context, ""),
- theme: option(opts, :theme, :light),
- pane_open?: option(opts, :pane_open?, false)
- }
+ shell:
+ %{
+ position: :top,
+ brand: option(opts, :brand, ""),
+ context: option(opts, :context, ""),
+ theme: option(opts, :theme, :light),
+ pane_open?: option(opts, :pane_open?, false)
+ }
+ |> maybe_put(:search_event, option(opts, :search_event))
+ |> maybe_put(:settings_event, option(opts, :settings_event))
+ |> maybe_put(:user_avatar_url, option(opts, :user_avatar_url))
},
Map.put(opts, :children, children)
)
diff --git a/packages/unified_iur/test/unified_iur/widgets/components_test.exs b/packages/unified_iur/test/unified_iur/widgets/components_test.exs
index fe650c6e..d291cebf 100644
--- a/packages/unified_iur/test/unified_iur/widgets/components_test.exs
+++ b/packages/unified_iur/test/unified_iur/widgets/components_test.exs
@@ -295,6 +295,56 @@ defmodule UnifiedIUR.Widgets.ComponentsTest do
assert code.attributes.text_safety == %{content: :plain_text}
end
+ test "top_strip stores trailing-region affordance attrs in shell map" do
+ strip_no_trailing = Components.top_strip([], brand: "Ariston", context: "Dashboard")
+
+ assert strip_no_trailing.kind == :top_strip
+ assert strip_no_trailing.attributes.shell.brand == "Ariston"
+ assert strip_no_trailing.attributes.shell.context == "Dashboard"
+ refute Map.has_key?(strip_no_trailing.attributes.shell, :search_event)
+ refute Map.has_key?(strip_no_trailing.attributes.shell, :settings_event)
+ refute Map.has_key?(strip_no_trailing.attributes.shell, :user_avatar_url)
+
+ strip_with_search =
+ Components.top_strip([], brand: "Ariston", search_event: "open_search")
+
+ assert strip_with_search.attributes.shell.search_event == "open_search"
+ refute Map.has_key?(strip_with_search.attributes.shell, :settings_event)
+ refute Map.has_key?(strip_with_search.attributes.shell, :user_avatar_url)
+
+ strip_with_settings =
+ Components.top_strip([], brand: "Ariston", settings_event: "open_settings")
+
+ assert strip_with_settings.attributes.shell.settings_event == "open_settings"
+
+ strip_with_avatar =
+ Components.top_strip([],
+ brand: "Ariston",
+ user_avatar_url: "https://example.com/avatar.png"
+ )
+
+ assert strip_with_avatar.attributes.shell.user_avatar_url == "https://example.com/avatar.png"
+
+ strip_all_trailing =
+ Components.top_strip([],
+ brand: "Ariston",
+ search_event: "open_search",
+ settings_event: "open_settings",
+ user_avatar_url: "https://example.com/avatar.png"
+ )
+
+ assert strip_all_trailing.attributes.shell == %{
+ position: :top,
+ brand: "Ariston",
+ context: "",
+ theme: :light,
+ pane_open?: false,
+ search_event: "open_search",
+ settings_event: "open_settings",
+ user_avatar_url: "https://example.com/avatar.png"
+ }
+ end
+
test "represents list repeat metadata and hydrated row children" do
template = Components.artifact_row("Template", [], row_identity: :id)
diff --git a/packages/unified_ui/lib/unified_ui/widget_components.ex b/packages/unified_ui/lib/unified_ui/widget_components.ex
index 513bd4c0..38fc703e 100644
--- a/packages/unified_ui/lib/unified_ui/widget_components.ex
+++ b/packages/unified_ui/lib/unified_ui/widget_components.ex
@@ -171,7 +171,7 @@ defmodule UnifiedUi.WidgetComponents do
kind: :top_strip,
family: :layer_shell_and_callout,
summary:
- "Top shell strip with brand, context, theme controls, and mode navigation children.",
+ "Top shell strip with brand, context, theme controls, mode navigation children, and optional trailing-region affordances (search_event, settings_event, user_avatar_url).",
aliases: []
},
%{