diff --git a/packages/desktop_ui/lib/desktop_ui/widgets/components.ex b/packages/desktop_ui/lib/desktop_ui/widgets/components.ex
index 0b8884e4..6ccf9fcc 100644
--- a/packages/desktop_ui/lib/desktop_ui/widgets/components.ex
+++ b/packages/desktop_ui/lib/desktop_ui/widgets/components.ex
@@ -16,7 +16,8 @@ defmodule DesktopUi.Widgets.Components do
@form_control_kinds [
:segmented_button_group,
:runtime_form_shell,
- :chat_composer
+ :chat_composer,
+ :mode_nav
]
@row_artifact_kinds [:list_item_multi_column, :artifact_row]
@@ -25,13 +26,19 @@ defmodule DesktopUi.Widgets.Components do
:pipeline_stepper_horizontal,
:segmented_progress_bar,
:workflow_stage_list_vertical,
- :meter_thin
+ :meter_thin,
+ :unread_badge
]
@layer_callout_kinds [
:sticky_frosted_header,
:slide_over_panel,
- :event_callout
+ :event_callout,
+ :top_strip,
+ :sidebar_shell,
+ :sidebar_section,
+ :sidebar_item,
+ :command_palette
]
@redline_code_kinds [:redline_inline, :code_block_syntax_highlighted]
@@ -55,6 +62,13 @@ defmodule DesktopUi.Widgets.Components do
sticky_frosted_header: :layer_callout,
slide_over_panel: :layer_callout,
event_callout: :layer_callout,
+ top_strip: :layer_callout,
+ sidebar_shell: :layer_callout,
+ sidebar_section: :layer_callout,
+ sidebar_item: :layer_callout,
+ command_palette: :layer_callout,
+ mode_nav: :form_control,
+ unread_badge: :workflow_progress,
redline_inline: :redline_code,
code_block_syntax_highlighted: :redline_code,
list_repeat: :composition_behavior
@@ -70,7 +84,10 @@ defmodule DesktopUi.Widgets.Components do
:pipeline_stepper_horizontal,
:workflow_stage_list_vertical,
:slide_over_panel,
- :event_callout
+ :event_callout,
+ :mode_nav,
+ :sidebar_item,
+ :command_palette
]
@kinds @content_identity_kinds ++
@@ -532,6 +549,150 @@ defmodule DesktopUi.Widgets.Components do
)
end
+ @spec mode_nav(String.t() | atom(), [keyword() | map()], keyword() | map()) :: Widget.t()
+ def mode_nav(id, items, opts \\ []) when is_list(items) do
+ opts = options(opts)
+
+ component_widget(
+ :mode_nav,
+ id,
+ %{
+ navigation:
+ %{items: normalize_maps(items)}
+ |> maybe_put(:aria_label, option(opts, :aria_label))
+ |> maybe_put(:navigation_intent, option(opts, :navigation_intent))
+ },
+ [],
+ opts,
+ on_navigate: :navigation
+ )
+ end
+
+ @spec unread_badge(String.t() | atom(), non_neg_integer(), keyword() | map()) :: Widget.t()
+ def unread_badge(id, count, opts \\ []) when is_integer(count) and count >= 0 do
+ opts = options(opts)
+
+ component_widget(
+ :unread_badge,
+ id,
+ %{status: %{count: count, threshold: option(opts, :threshold, 99)}},
+ [],
+ opts
+ )
+ end
+
+ @spec top_strip(String.t() | atom(), [Widget.t() | map() | keyword()], keyword() | map()) ::
+ Widget.t()
+ def top_strip(id, children \\ [], opts \\ []) when is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :top_strip,
+ id,
+ %{
+ shell: %{
+ position: :top,
+ brand: option(opts, :brand, ""),
+ context: option(opts, :context, ""),
+ theme: option(opts, :theme, :light),
+ pane_open?: option(opts, :pane_open?, false)
+ }
+ },
+ children,
+ opts
+ )
+ end
+
+ @spec sidebar_shell(String.t() | atom(), [Widget.t() | map() | keyword()], keyword() | map()) ::
+ Widget.t()
+ def sidebar_shell(id, children \\ [], opts \\ []) when is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :sidebar_shell,
+ id,
+ %{shell: %{position: :side, collapsed?: option(opts, :collapsed?, false)}},
+ children,
+ opts
+ )
+ end
+
+ @spec sidebar_section(
+ String.t() | atom(),
+ String.t(),
+ [Widget.t() | map() | keyword()],
+ keyword() | map()
+ ) :: Widget.t()
+ def sidebar_section(id, label, children \\ [], opts \\ [])
+ when is_binary(label) and is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :sidebar_section,
+ id,
+ %{
+ section:
+ %{label: label}
+ |> maybe_put(:action_glyph, option(opts, :action_glyph))
+ |> maybe_put(:action_label, option(opts, :action_label))
+ |> maybe_put(:action_intent, option(opts, :action_intent))
+ },
+ children,
+ opts
+ )
+ end
+
+ @spec sidebar_item(
+ String.t() | atom(),
+ String.t(),
+ [Widget.t() | map() | keyword()],
+ keyword() | map()
+ ) :: Widget.t()
+ def sidebar_item(id, label, children \\ [], opts \\ [])
+ when is_binary(label) and is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :sidebar_item,
+ id,
+ %{
+ item:
+ %{label: label, selected?: option(opts, :selected?, false)}
+ |> maybe_put(:item_intent, option(opts, :item_intent))
+ },
+ children,
+ opts,
+ on_select: :selection,
+ on_click: :click
+ )
+ end
+
+ @spec command_palette(
+ String.t() | atom(),
+ [keyword() | map()],
+ [Widget.t() | map() | keyword()],
+ keyword() | map()
+ ) :: Widget.t()
+ def command_palette(id, items \\ [], children \\ [], opts \\ [])
+ when is_list(items) and is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :command_palette,
+ id,
+ %{
+ palette:
+ %{open?: option(opts, :open?, false), items: normalize_maps(items)}
+ |> maybe_put(:filter_intent, option(opts, :filter_intent))
+ |> maybe_put(:select_intent, option(opts, :select_intent))
+ },
+ children,
+ opts,
+ on_filter: :filter,
+ on_select: :select
+ )
+ end
+
@spec from_iur(atom(), String.t() | atom(), map(), keyword() | map()) :: Widget.t()
def from_iur(kind, id, attributes, opts \\ []) when is_atom(kind) and is_map(attributes) do
opts = options(opts)
diff --git a/packages/desktop_ui/test/desktop_ui/phase_eleven_integration_test.exs b/packages/desktop_ui/test/desktop_ui/phase_eleven_integration_test.exs
index ebde1a59..7197b27e 100644
--- a/packages/desktop_ui/test/desktop_ui/phase_eleven_integration_test.exs
+++ b/packages/desktop_ui/test/desktop_ui/phase_eleven_integration_test.exs
@@ -8,7 +8,7 @@ defmodule DesktopUi.PhaseElevenIntegrationTest do
@moduletag :phase_eleven
- @iur_widget_count 77
+ @iur_widget_count 83
@all_iur_kinds MapSet.new([
# Foundational (13)
@@ -77,7 +77,7 @@ defmodule DesktopUi.PhaseElevenIntegrationTest do
:stack,
# Container (1)
:window,
- # Canonical widget components (20)
+ # Canonical widget components (27)
:inline_rich_text_heading,
:disclosure,
:kicker,
@@ -86,15 +86,22 @@ defmodule DesktopUi.PhaseElevenIntegrationTest do
:segmented_button_group,
:runtime_form_shell,
:chat_composer,
+ :mode_nav,
:list_item_multi_column,
:artifact_row,
:pipeline_stepper_horizontal,
:segmented_progress_bar,
:workflow_stage_list_vertical,
:meter_thin,
+ :unread_badge,
:sticky_frosted_header,
:slide_over_panel,
:event_callout,
+ :top_strip,
+ :sidebar_shell,
+ :sidebar_section,
+ :sidebar_item,
+ :command_palette,
:redline_inline,
:code_block_syntax_highlighted,
:list_repeat
diff --git a/packages/elm_ui/lib/elm_ui/widgets/components.ex b/packages/elm_ui/lib/elm_ui/widgets/components.ex
index ea703584..014be66a 100644
--- a/packages/elm_ui/lib/elm_ui/widgets/components.ex
+++ b/packages/elm_ui/lib/elm_ui/widgets/components.ex
@@ -19,7 +19,8 @@ defmodule ElmUi.Widgets.Components do
@form_control_kinds [
:segmented_button_group,
:runtime_form_shell,
- :chat_composer
+ :chat_composer,
+ :mode_nav
]
@row_artifact_kinds [
@@ -31,13 +32,19 @@ defmodule ElmUi.Widgets.Components do
:pipeline_stepper_horizontal,
:segmented_progress_bar,
:workflow_stage_list_vertical,
- :meter_thin
+ :meter_thin,
+ :unread_badge
]
@layer_callout_kinds [
:sticky_frosted_header,
:slide_over_panel,
- :event_callout
+ :event_callout,
+ :top_strip,
+ :sidebar_shell,
+ :sidebar_section,
+ :sidebar_item,
+ :command_palette
]
@redline_code_kinds [
@@ -67,6 +74,13 @@ defmodule ElmUi.Widgets.Components do
sticky_frosted_header: :layer_callout,
slide_over_panel: :layer_callout,
event_callout: :layer_callout,
+ top_strip: :layer_callout,
+ sidebar_shell: :layer_callout,
+ sidebar_section: :layer_callout,
+ sidebar_item: :layer_callout,
+ command_palette: :layer_callout,
+ mode_nav: :form_control,
+ unread_badge: :workflow_progress,
redline_inline: :redline_code,
code_block_syntax_highlighted: :redline_code,
list_repeat: :composition_behavior
@@ -590,6 +604,162 @@ defmodule ElmUi.Widgets.Components do
)
end
+ @spec mode_nav(String.t() | atom(), [keyword() | map()], keyword() | map()) :: ElmUi.Widget.t()
+ def mode_nav(id, items, opts \\ []) when is_list(items) do
+ opts = options(opts)
+
+ component_widget(
+ :mode_nav,
+ id,
+ %{
+ navigation:
+ %{items: normalize_maps(items)}
+ |> maybe_put(:aria_label, Builder.option(opts, :aria_label))
+ |> maybe_put(:navigation_intent, Builder.option(opts, :navigation_intent))
+ },
+ [],
+ opts,
+ on_navigate: :navigation
+ )
+ end
+
+ @spec unread_badge(String.t() | atom(), non_neg_integer(), keyword() | map()) ::
+ ElmUi.Widget.t()
+ def unread_badge(id, count, opts \\ []) when is_integer(count) and count >= 0 do
+ opts = options(opts)
+
+ component_widget(
+ :unread_badge,
+ id,
+ %{
+ status: %{
+ count: count,
+ threshold: Builder.option(opts, :threshold, 99)
+ }
+ },
+ [],
+ opts
+ )
+ end
+
+ @spec top_strip(
+ String.t() | atom(),
+ [ElmUi.Widget.t() | map() | keyword()],
+ keyword() | map()
+ ) :: ElmUi.Widget.t()
+ def top_strip(id, children \\ [], opts \\ []) when is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :top_strip,
+ id,
+ %{
+ shell: %{
+ position: :top,
+ brand: Builder.option(opts, :brand, ""),
+ context: Builder.option(opts, :context, ""),
+ theme: Builder.option(opts, :theme, :light),
+ pane_open?: Builder.option(opts, :pane_open?, false)
+ }
+ },
+ children,
+ opts
+ )
+ end
+
+ @spec sidebar_shell(
+ String.t() | atom(),
+ [ElmUi.Widget.t() | map() | keyword()],
+ keyword() | map()
+ ) :: ElmUi.Widget.t()
+ def sidebar_shell(id, children \\ [], opts \\ []) when is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :sidebar_shell,
+ id,
+ %{shell: %{position: :side, collapsed?: Builder.option(opts, :collapsed?, false)}},
+ children,
+ opts
+ )
+ end
+
+ @spec sidebar_section(
+ String.t() | atom(),
+ String.t(),
+ [ElmUi.Widget.t() | map() | keyword()],
+ keyword() | map()
+ ) :: ElmUi.Widget.t()
+ def sidebar_section(id, label, children \\ [], opts \\ [])
+ when is_binary(label) and is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :sidebar_section,
+ id,
+ %{
+ section:
+ %{label: label}
+ |> maybe_put(:action_glyph, Builder.option(opts, :action_glyph))
+ |> maybe_put(:action_label, Builder.option(opts, :action_label))
+ |> maybe_put(:action_intent, Builder.option(opts, :action_intent))
+ },
+ children,
+ opts
+ )
+ end
+
+ @spec sidebar_item(
+ String.t() | atom(),
+ String.t(),
+ [ElmUi.Widget.t() | map() | keyword()],
+ keyword() | map()
+ ) :: ElmUi.Widget.t()
+ def sidebar_item(id, label, children \\ [], opts \\ [])
+ when is_binary(label) and is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :sidebar_item,
+ id,
+ %{
+ item:
+ %{label: label, selected?: Builder.option(opts, :selected?, false)}
+ |> maybe_put(:item_intent, Builder.option(opts, :item_intent))
+ },
+ children,
+ opts,
+ on_select: :selection,
+ on_click: :click
+ )
+ end
+
+ @spec command_palette(
+ String.t() | atom(),
+ [keyword() | map()],
+ [ElmUi.Widget.t() | map() | keyword()],
+ keyword() | map()
+ ) :: ElmUi.Widget.t()
+ def command_palette(id, items \\ [], children \\ [], opts \\ [])
+ when is_list(items) and is_list(children) do
+ opts = options(opts)
+
+ component_widget(
+ :command_palette,
+ id,
+ %{
+ palette:
+ %{open?: Builder.option(opts, :open?, false), items: normalize_maps(items)}
+ |> maybe_put(:filter_intent, Builder.option(opts, :filter_intent))
+ |> maybe_put(:select_intent, Builder.option(opts, :select_intent))
+ },
+ children,
+ opts,
+ on_filter: :filter,
+ on_select: :select
+ )
+ end
+
@spec from_iur(
atom(),
String.t() | atom() | nil,
diff --git a/packages/live_ui/lib/live_ui/renderer.ex b/packages/live_ui/lib/live_ui/renderer.ex
index 27d05339..764c5ab9 100644
--- a/packages/live_ui/lib/live_ui/renderer.ex
+++ b/packages/live_ui/lib/live_ui/renderer.ex
@@ -578,7 +578,11 @@ defmodule LiveUi.Renderer do
"""
end
- def render(%{element: %Element{kind: :command_palette}} = assigns) do
+ def render(
+ %{
+ element: %Element{kind: :command_palette, attributes: %{command_palette: _}}
+ } = assigns
+ ) do
assigns =
assigns
|> assign(
@@ -1480,6 +1484,172 @@ defmodule LiveUi.Renderer do
"""
end
+ def render(%{element: %Element{kind: :top_strip}} = assigns) do
+ assigns = assign(assigns, :style_attrs, style_rest(assigns.element))
+
+ ~H"""
+
+ <%= for child <- child_elements(@element) do %>
+ <.render element={child} event_target={@event_target} />
+ <% end %>
+
+ """
+ end
+
+ def render(%{element: %Element{kind: :mode_nav}} = assigns) do
+ assigns =
+ assign(
+ assigns,
+ :nav_attrs,
+ interaction_event_attrs(assigns.element, Map.get(assigns, :event_target))
+ )
+
+ ~H"""
+
+ """
+ end
+
+ def render(%{element: %Element{kind: :sidebar_shell}} = assigns) do
+ assigns = assign(assigns, :style_attrs, style_rest(assigns.element))
+
+ ~H"""
+
+ <%= for child <- child_elements(@element) do %>
+ <.render element={child} event_target={@event_target} />
+ <% end %>
+
+ """
+ end
+
+ def render(%{element: %Element{kind: :sidebar_section}} = assigns) do
+ assigns =
+ assign(
+ assigns,
+ :action_attrs,
+ interaction_event_attrs(assigns.element, Map.get(assigns, :event_target))
+ )
+
+ ~H"""
+
+ <%= for child <- child_elements(@element) do %>
+ <.render element={child} event_target={@event_target} />
+ <% end %>
+
+ """
+ end
+
+ def render(%{element: %Element{kind: :sidebar_item}} = assigns) do
+ assigns =
+ assign(
+ assigns,
+ :item_attrs,
+ interaction_event_attrs(assigns.element, Map.get(assigns, :event_target))
+ )
+
+ ~H"""
+
+ <%= for child <- child_elements(@element) do %>
+ <.render element={child} event_target={@event_target} />
+ <% end %>
+
+ """
+ end
+
+ def render(%{element: %Element{kind: :unread_badge}} = assigns) do
+ ~H"""
+
+ """
+ end
+
+ def render(
+ %{element: %Element{kind: :command_palette, attributes: %{component: %{family: _}}}} =
+ assigns
+ ) do
+ assigns =
+ assigns
+ |> assign(
+ :filter_attrs,
+ direct_change_interaction_attrs(assigns.element, Map.get(assigns, :event_target))
+ )
+ |> assign(
+ :select_attrs,
+ interaction_event_attrs(assigns.element, Map.get(assigns, :event_target))
+ )
+
+ ~H"""
+
+ <%= for child <- child_elements(@element) do %>
+ <.render element={child} event_target={@event_target} />
+ <% end %>
+
+ """
+ end
+
def render(assigns) do
~H"""
diff --git a/packages/live_ui/lib/live_ui/widgets/components.ex b/packages/live_ui/lib/live_ui/widgets/components.ex
index dbe87b8a..cc1cb186 100644
--- a/packages/live_ui/lib/live_ui/widgets/components.ex
+++ b/packages/live_ui/lib/live_ui/widgets/components.ex
@@ -32,7 +32,8 @@ defmodule LiveUi.Widgets.Components do
@form_control_modules [
LiveUi.Widgets.Components.SegmentedButtonGroup,
LiveUi.Widgets.Components.RuntimeFormShell,
- LiveUi.Widgets.Components.ChatComposer
+ LiveUi.Widgets.Components.ChatComposer,
+ LiveUi.Widgets.Components.ModeNav
]
@row_artifact_modules [
@@ -44,13 +45,19 @@ defmodule LiveUi.Widgets.Components do
LiveUi.Widgets.Components.PipelineStepperHorizontal,
LiveUi.Widgets.Components.SegmentedProgressBar,
LiveUi.Widgets.Components.WorkflowStageListVertical,
- LiveUi.Widgets.Components.MeterThin
+ LiveUi.Widgets.Components.MeterThin,
+ LiveUi.Widgets.Components.UnreadBadge
]
@layer_callout_modules [
LiveUi.Widgets.Components.StickyFrostedHeader,
LiveUi.Widgets.Components.SlideOverPanel,
- LiveUi.Widgets.Components.EventCallout
+ LiveUi.Widgets.Components.EventCallout,
+ LiveUi.Widgets.Components.TopStrip,
+ LiveUi.Widgets.Components.SidebarShell,
+ LiveUi.Widgets.Components.SidebarSection,
+ LiveUi.Widgets.Components.SidebarItem,
+ LiveUi.Widgets.Components.CommandPalette
]
@redline_code_modules [
@@ -232,6 +239,62 @@ defmodule LiveUi.Widgets.Components do
slots: [:inner_block],
events: [],
local_state_keys: []
+ },
+ mode_nav: %{
+ module: LiveUi.Widgets.Components.ModeNav,
+ family: :form_control,
+ assigns: [:items, :aria_label, :nav_attrs],
+ slots: [],
+ events: [:navigation],
+ local_state_keys: []
+ },
+ unread_badge: %{
+ module: LiveUi.Widgets.Components.UnreadBadge,
+ family: :workflow_progress,
+ assigns: [:count, :threshold],
+ slots: [],
+ events: [],
+ local_state_keys: []
+ },
+ top_strip: %{
+ module: LiveUi.Widgets.Components.TopStrip,
+ family: :layer_callout,
+ assigns: [:brand, :context, :theme, :pane_open],
+ slots: [:inner_block],
+ events: [],
+ local_state_keys: []
+ },
+ sidebar_shell: %{
+ module: LiveUi.Widgets.Components.SidebarShell,
+ family: :layer_callout,
+ assigns: [:collapsed],
+ slots: [:inner_block],
+ events: [],
+ local_state_keys: []
+ },
+ sidebar_section: %{
+ module: LiveUi.Widgets.Components.SidebarSection,
+ family: :layer_callout,
+ assigns: [:label, :action_glyph, :action_label, :action_attrs],
+ slots: [:inner_block],
+ events: [],
+ local_state_keys: []
+ },
+ sidebar_item: %{
+ module: LiveUi.Widgets.Components.SidebarItem,
+ family: :layer_callout,
+ assigns: [:label, :selected, :item_attrs],
+ slots: [:inner_block],
+ events: [],
+ local_state_keys: []
+ },
+ command_palette: %{
+ module: LiveUi.Widgets.Components.CommandPalette,
+ family: :layer_callout,
+ assigns: [:open, :items, :filter_attrs, :select_attrs],
+ slots: [:inner_block],
+ events: [:filter, :select],
+ local_state_keys: []
}
}
diff --git a/packages/live_ui/lib/live_ui/widgets/components/form_control.ex b/packages/live_ui/lib/live_ui/widgets/components/form_control.ex
index 6786b345..6da96078 100644
--- a/packages/live_ui/lib/live_ui/widgets/components/form_control.ex
+++ b/packages/live_ui/lib/live_ui/widgets/components/form_control.ex
@@ -102,6 +102,55 @@ defmodule LiveUi.Widgets.Components.RuntimeFormShell do
end
end
+defmodule LiveUi.Widgets.Components.ModeNav do
+ @moduledoc """
+ Native navigation control for switching application modes with labeled items and shortcuts.
+ """
+
+ alias LiveUi.Widgets.Components.Support
+
+ use LiveUi.Component,
+ family: :components,
+ name: :mode_nav,
+ assigns: [:items, :aria_label, :nav_attrs],
+ events: [:navigation]
+
+ LiveUi.Component.common_attrs()
+ attr(:items, :list, default: [])
+ attr(:aria_label, :string, default: nil)
+ attr(:nav_attrs, :map, default: %{})
+
+ @impl true
+ def render(assigns) do
+ assigns =
+ assign(
+ assigns,
+ :component_attrs,
+ Support.component_attrs(assigns, :mode_nav, :form_control)
+ )
+
+ ~H"""
+
+ """
+ end
+end
+
defmodule LiveUi.Widgets.Components.ChatComposer do
@moduledoc """
Native chat composer with message text and tool slots.
diff --git a/packages/live_ui/lib/live_ui/widgets/components/layer_text.ex b/packages/live_ui/lib/live_ui/widgets/components/layer_text.ex
index 7b5ee6f7..21618c31 100644
--- a/packages/live_ui/lib/live_ui/widgets/components/layer_text.ex
+++ b/packages/live_ui/lib/live_ui/widgets/components/layer_text.ex
@@ -210,6 +210,239 @@ defmodule LiveUi.Widgets.Components.CodeBlockSyntaxHighlighted do
end
end
+defmodule LiveUi.Widgets.Components.TopStrip do
+ @moduledoc """
+ Native top shell strip with brand identity, context, theme controls, and mode navigation.
+ """
+
+ alias LiveUi.Widgets.Components.Support
+
+ use LiveUi.Component,
+ family: :components,
+ name: :top_strip,
+ slots: [:inner_block],
+ assigns: [:brand, :context, :theme, :pane_open]
+
+ LiveUi.Component.common_attrs()
+ attr(:brand, :string, default: "")
+ attr(:context, :string, default: "")
+ attr(:theme, :string, default: "light")
+ attr(:pane_open, :boolean, default: false)
+ slot(:inner_block)
+
+ @impl true
+ def render(assigns) do
+ assigns =
+ assign(
+ assigns,
+ :component_attrs,
+ Support.component_attrs(assigns, :top_strip, :layer_callout, %{
+ "data-live-ui-shell-position" => "top",
+ "data-live-ui-theme" => assigns.theme,
+ "data-live-ui-pane-open" => assigns.pane_open
+ })
+ )
+
+ ~H"""
+
+ """
+ end
+end
+
+defmodule LiveUi.Widgets.Components.SidebarShell do
+ @moduledoc """
+ Native side navigation shell with collapsible state and section children.
+ """
+
+ alias LiveUi.Widgets.Components.Support
+
+ use LiveUi.Component,
+ family: :components,
+ name: :sidebar_shell,
+ slots: [:inner_block],
+ assigns: [:collapsed]
+
+ LiveUi.Component.common_attrs()
+ attr(:collapsed, :boolean, default: false)
+ slot(:inner_block)
+
+ @impl true
+ def render(assigns) do
+ assigns =
+ assign(
+ assigns,
+ :component_attrs,
+ Support.component_attrs(assigns, :sidebar_shell, :layer_callout, %{
+ "data-live-ui-shell-position" => "side",
+ "data-live-ui-collapsed" => assigns.collapsed
+ })
+ )
+
+ ~H"""
+
+ """
+ end
+end
+
+defmodule LiveUi.Widgets.Components.SidebarSection do
+ @moduledoc """
+ Native labeled section group inside a sidebar shell with optional action and item children.
+ """
+
+ alias LiveUi.Widgets.Components.Support
+
+ use LiveUi.Component,
+ family: :components,
+ name: :sidebar_section,
+ slots: [:inner_block],
+ assigns: [:label, :action_glyph, :action_label, :action_attrs]
+
+ LiveUi.Component.common_attrs()
+ attr(:label, :string, required: true)
+ attr(:action_glyph, :string, default: nil)
+ attr(:action_label, :string, default: nil)
+ attr(:action_attrs, :map, default: %{})
+ slot(:inner_block)
+
+ @impl true
+ def render(assigns) do
+ assigns =
+ assign(
+ assigns,
+ :component_attrs,
+ Support.component_attrs(assigns, :sidebar_section, :layer_callout)
+ )
+
+ ~H"""
+
+
+ <%= @label %>
+
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+end
+
+defmodule LiveUi.Widgets.Components.SidebarItem do
+ @moduledoc """
+ Native navigable sidebar item with selected state, intent, and optional badge children.
+ """
+
+ alias LiveUi.Widgets.Components.Support
+
+ use LiveUi.Component,
+ family: :components,
+ name: :sidebar_item,
+ slots: [:inner_block],
+ assigns: [:label, :selected, :item_attrs]
+
+ LiveUi.Component.common_attrs()
+ attr(:label, :string, required: true)
+ attr(:selected, :boolean, default: false)
+ attr(:item_attrs, :map, default: %{})
+ slot(:inner_block)
+
+ @impl true
+ def render(assigns) do
+ assigns =
+ assign(
+ assigns,
+ :component_attrs,
+ Support.component_attrs(assigns, :sidebar_item, :layer_callout, %{
+ "data-live-ui-selected" => assigns.selected
+ })
+ )
+
+ ~H"""
+
+
+
+ """
+ end
+end
+
+defmodule LiveUi.Widgets.Components.CommandPalette do
+ @moduledoc """
+ Native keyboard-driven command palette overlay with open state, filterable items, and children.
+ """
+
+ alias LiveUi.Widgets.Components.Support
+
+ use LiveUi.Component,
+ family: :components,
+ name: :command_palette,
+ slots: [:inner_block],
+ assigns: [:open, :items, :filter_attrs, :select_attrs],
+ events: [:filter, :select]
+
+ LiveUi.Component.common_attrs()
+ attr(:open, :boolean, default: false)
+ attr(:items, :list, default: [])
+ attr(:filter_attrs, :map, default: %{})
+ attr(:select_attrs, :map, default: %{})
+ slot(:inner_block)
+
+ @impl true
+ def render(assigns) do
+ assigns =
+ assign(
+ assigns,
+ :component_attrs,
+ Support.component_attrs(assigns, :command_palette, :layer_callout, %{
+ "data-live-ui-palette-open" => assigns.open
+ })
+ )
+
+ ~H"""
+
+ """
+ end
+end
+
defmodule LiveUi.Widgets.Components.ListRepeat do
@moduledoc """
Native wrapper for already-hydrated list-repeat child output.
diff --git a/packages/live_ui/lib/live_ui/widgets/components/row_workflow.ex b/packages/live_ui/lib/live_ui/widgets/components/row_workflow.ex
index 17bfb480..677a97e7 100644
--- a/packages/live_ui/lib/live_ui/widgets/components/row_workflow.ex
+++ b/packages/live_ui/lib/live_ui/widgets/components/row_workflow.ex
@@ -295,3 +295,47 @@ defmodule LiveUi.Widgets.Components.MeterThin do
"""
end
end
+
+defmodule LiveUi.Widgets.Components.UnreadBadge do
+ @moduledoc """
+ Native compact unread count badge with threshold capping and hidden-when-zero behavior.
+ """
+
+ alias LiveUi.Widgets.Components.Support
+
+ use LiveUi.Component,
+ family: :components,
+ name: :unread_badge,
+ assigns: [:count, :threshold]
+
+ LiveUi.Component.common_attrs()
+ attr(:count, :integer, default: 0)
+ attr(:threshold, :integer, default: 99)
+
+ @impl true
+ def render(assigns) do
+ assigns =
+ assigns
+ |> assign(:display_count, display_count(assigns.count, assigns.threshold))
+ |> assign(
+ :component_attrs,
+ Support.component_attrs(assigns, :unread_badge, :workflow_progress, %{
+ "data-live-ui-unread-count" => assigns.count
+ })
+ )
+
+ ~H"""
+
0}
+ id={@id}
+ role="status"
+ aria-label={"#{@count} unread"}
+ class={@class}
+ {@component_attrs}
+ ><%= @display_count %>
+ """
+ end
+
+ defp display_count(count, threshold) when count > threshold, do: "#{threshold}+"
+ defp display_count(count, _threshold), do: to_string(count)
+end
diff --git a/packages/live_ui/test/live_ui/widget_components_shell_primitives_test.exs b/packages/live_ui/test/live_ui/widget_components_shell_primitives_test.exs
new file mode 100644
index 00000000..87004099
--- /dev/null
+++ b/packages/live_ui/test/live_ui/widget_components_shell_primitives_test.exs
@@ -0,0 +1,235 @@
+defmodule LiveUi.WidgetComponentsShellPrimitivesTest do
+ use ExUnit.Case, async: true
+
+ import Phoenix.LiveViewTest
+
+ defp slot_text(slot, text) do
+ %{__slot__: slot, inner_block: fn _, _ -> text end}
+ end
+
+ test "top_strip renders shell header with brand, context, and theme attrs" do
+ html =
+ render_component(&LiveUi.Widgets.Components.TopStrip.component/1, %{
+ id: "top-strip",
+ brand: "Ariston",
+ context: "Workspace",
+ theme: "dark",
+ pane_open: false,
+ inner_block: [slot_text(:inner_block, "nav-content")]
+ })
+
+ assert html =~ ~s(data-live-ui-shell-position="top")
+ assert html =~ ~s(data-live-ui-theme="dark")
+ assert html =~ "Ariston"
+ assert html =~ "Workspace"
+ assert html =~ "nav-content"
+ end
+
+ test "top_strip omits brand span when brand is empty" do
+ html =
+ render_component(&LiveUi.Widgets.Components.TopStrip.component/1, %{
+ id: "strip-no-brand",
+ brand: "",
+ pane_open: false,
+ inner_block: [slot_text(:inner_block, "")]
+ })
+
+ refute html =~ "data-live-ui-strip-brand"
+ end
+
+ test "mode_nav renders navigation with item labels and current state" do
+ html =
+ render_component(&LiveUi.Widgets.Components.ModeNav.component/1, %{
+ id: "mode-nav",
+ aria_label: "App modes",
+ items: [
+ %{value: "workspace", label: "Workspace", current?: true},
+ %{value: "map", label: "Map", shortcut: "m"}
+ ]
+ })
+
+ assert html =~ ~s(role="navigation")
+ assert html =~ ~s(aria-label="App modes")
+ assert html =~ ~s(data-live-ui-nav-value="workspace")
+ assert html =~ ~s(aria-current="page")
+ assert html =~ "Workspace"
+ assert html =~ "Map"
+ assert html =~ ~s(data-live-ui-nav-shortcut="m")
+ end
+
+ test "mode_nav item without current? has aria-current false" do
+ html =
+ render_component(&LiveUi.Widgets.Components.ModeNav.component/1, %{
+ id: "mode-nav-2",
+ items: [%{value: "docs", label: "Docs"}]
+ })
+
+ assert html =~ ~s(aria-current="false")
+ refute html =~ ~s(aria-current="page")
+ end
+
+ test "sidebar_shell renders nav with collapsed data attribute" do
+ html =
+ render_component(&LiveUi.Widgets.Components.SidebarShell.component/1, %{
+ id: "sidebar",
+ collapsed: true,
+ inner_block: [slot_text(:inner_block, "sections")]
+ })
+
+ assert html =~ ~s(data-live-ui-shell-position="side")
+ assert html =~ ~s(data-live-ui-collapsed="true")
+ assert html =~ ~s(aria-hidden="true")
+ assert html =~ "sections"
+ end
+
+ test "sidebar_shell when expanded has aria-hidden false" do
+ html =
+ render_component(&LiveUi.Widgets.Components.SidebarShell.component/1, %{
+ id: "sidebar-open",
+ collapsed: false,
+ inner_block: [slot_text(:inner_block, "")]
+ })
+
+ assert html =~ ~s(aria-hidden="false")
+ end
+
+ test "sidebar_section renders labeled section with action button" do
+ html =
+ render_component(&LiveUi.Widgets.Components.SidebarSection.component/1, %{
+ id: "section",
+ label: "Specs",
+ action_label: "Add spec",
+ action_glyph: "+",
+ action_attrs: %{"phx-click" => "add_spec"},
+ inner_block: [slot_text(:inner_block, "items")]
+ })
+
+ assert html =~ ~s(data-live-ui-shell-section)
+ assert html =~ "Specs"
+ assert html =~ ~s(aria-label="Add spec")
+ assert html =~ "+"
+ assert html =~ ~s(phx-click="add_spec")
+ assert html =~ "items"
+ end
+
+ test "sidebar_section without action renders no action button" do
+ html =
+ render_component(&LiveUi.Widgets.Components.SidebarSection.component/1, %{
+ id: "section-no-action",
+ label: "Repos",
+ inner_block: [slot_text(:inner_block, "")]
+ })
+
+ refute html =~ "data-live-ui-section-action"
+ end
+
+ test "sidebar_item renders selected item with aria-current page" do
+ html =
+ render_component(&LiveUi.Widgets.Components.SidebarItem.component/1, %{
+ id: "item-selected",
+ label: "metagraph",
+ selected: true,
+ inner_block: [slot_text(:inner_block, "")]
+ })
+
+ assert html =~ ~s(data-live-ui-shell-item)
+ assert html =~ ~s(aria-current="page")
+ assert html =~ "metagraph"
+ end
+
+ test "sidebar_item renders unselected item with aria-current false" do
+ html =
+ render_component(&LiveUi.Widgets.Components.SidebarItem.component/1, %{
+ id: "item-unselected",
+ label: "ariston-ui",
+ selected: false,
+ inner_block: [slot_text(:inner_block, "")]
+ })
+
+ assert html =~ ~s(aria-current="false")
+ refute html =~ ~s(aria-current="page")
+ end
+
+ test "sidebar_item renders badge children in slot" do
+ html =
+ render_component(&LiveUi.Widgets.Components.SidebarItem.component/1, %{
+ id: "item-with-badge",
+ label: "inbox",
+ selected: false,
+ inner_block: [slot_text(:inner_block, "
3")]
+ })
+
+ assert html =~ "inbox"
+ assert html =~ "3"
+ end
+
+ test "unread_badge renders count with role status" do
+ html =
+ render_component(&LiveUi.Widgets.Components.UnreadBadge.component/1, %{
+ id: "badge",
+ count: 5,
+ threshold: 99
+ })
+
+ assert html =~ ~s(role="status")
+ assert html =~ ~s(data-live-ui-unread-count="5")
+ assert html =~ ~s(aria-label="5 unread")
+ assert html =~ "5"
+ end
+
+ test "unread_badge caps display at threshold" do
+ html =
+ render_component(&LiveUi.Widgets.Components.UnreadBadge.component/1, %{
+ id: "badge-overflow",
+ count: 150,
+ threshold: 99
+ })
+
+ assert html =~ "99+"
+ assert html =~ ~s(data-live-ui-unread-count="150")
+ end
+
+ test "unread_badge renders nothing when count is zero" do
+ html =
+ render_component(&LiveUi.Widgets.Components.UnreadBadge.component/1, %{
+ id: "badge-zero",
+ count: 0
+ })
+
+ refute html =~ ~s(role="status")
+ refute html =~ "0"
+ end
+
+ test "command_palette renders overlay with open state and items" do
+ html =
+ render_component(&LiveUi.Widgets.Components.CommandPalette.component/1, %{
+ id: "palette",
+ open: true,
+ items: [
+ %{id: "goto-spec", label: "Go to spec", active: true},
+ %{id: "new-doc", label: "New document", active: false}
+ ],
+ inner_block: [slot_text(:inner_block, "")]
+ })
+
+ assert html =~ ~s(role="dialog")
+ assert html =~ ~s(aria-modal="true")
+ assert html =~ ~s(aria-hidden="false")
+ assert html =~ ~s(data-live-ui-palette-open="true")
+ assert html =~ ~s(data-live-ui-palette-item="goto-spec")
+ assert html =~ "Go to spec"
+ assert html =~ "New document"
+ end
+
+ test "command_palette with open false has aria-hidden true" do
+ html =
+ render_component(&LiveUi.Widgets.Components.CommandPalette.component/1, %{
+ id: "palette-closed",
+ open: false,
+ items: []
+ })
+
+ assert html =~ ~s(aria-hidden="true")
+ assert html =~ ~s(data-live-ui-palette-open="false")
+ end
+end
diff --git a/packages/unified-ui/lib/unified_ui/widget_components.ex b/packages/unified-ui/lib/unified_ui/widget_components.ex
index b349c710..513bd4c0 100644
--- a/packages/unified-ui/lib/unified_ui/widget_components.ex
+++ b/packages/unified-ui/lib/unified_ui/widget_components.ex
@@ -166,6 +166,52 @@ defmodule UnifiedUi.WidgetComponents do
summary:
"Composition behavior that repeats a child template over rows from a list binding.",
aliases: [:ui_relationship_repeat, :repeat]
+ },
+ %{
+ kind: :top_strip,
+ family: :layer_shell_and_callout,
+ summary:
+ "Top shell strip with brand, context, theme controls, and mode navigation children.",
+ aliases: []
+ },
+ %{
+ kind: :mode_nav,
+ family: :form_control_and_composer,
+ summary:
+ "Navigation control for switching application modes with labeled items and shortcuts.",
+ aliases: []
+ },
+ %{
+ kind: :sidebar_shell,
+ family: :layer_shell_and_callout,
+ summary: "Side navigation shell with collapsible state and sidebar section children.",
+ aliases: []
+ },
+ %{
+ kind: :sidebar_section,
+ family: :layer_shell_and_callout,
+ summary:
+ "Labeled section group inside a sidebar shell with optional action and item children.",
+ aliases: []
+ },
+ %{
+ kind: :sidebar_item,
+ family: :layer_shell_and_callout,
+ summary: "Navigable sidebar item with selected state, intent, and optional badge children.",
+ aliases: []
+ },
+ %{
+ kind: :unread_badge,
+ family: :workflow_progress_and_status,
+ summary: "Compact unread count badge with threshold capping.",
+ aliases: []
+ },
+ %{
+ kind: :command_palette,
+ family: :layer_shell_and_callout,
+ summary:
+ "Keyboard-driven command palette overlay with open state, filterable items, and children.",
+ aliases: []
}
]
diff --git a/packages/unified-ui/test/unified_ui/widget_components_catalog_test.exs b/packages/unified-ui/test/unified_ui/widget_components_catalog_test.exs
index 31f96ee1..6243fc52 100644
--- a/packages/unified-ui/test/unified_ui/widget_components_catalog_test.exs
+++ b/packages/unified-ui/test/unified_ui/widget_components_catalog_test.exs
@@ -25,19 +25,26 @@ defmodule UnifiedUi.WidgetComponentsCatalogTest do
form_control_and_composer: [
:runtime_form_shell,
:segmented_button_group,
- :chat_composer
+ :chat_composer,
+ :mode_nav
],
row_and_artifact: [:list_item_multi_column, :artifact_row],
workflow_progress_and_status: [
:pipeline_stepper_horizontal,
:segmented_progress_bar,
:workflow_stage_list_vertical,
- :meter_thin
+ :meter_thin,
+ :unread_badge
],
layer_shell_and_callout: [
:sticky_frosted_header,
:slide_over_panel,
- :event_callout
+ :event_callout,
+ :top_strip,
+ :sidebar_shell,
+ :sidebar_section,
+ :sidebar_item,
+ :command_palette
],
redline_and_code: [:redline_inline, :code_block_syntax_highlighted],
composition_behavior: [:list_repeat]
diff --git a/packages/unified_iur/lib/unified_iur/fixtures.ex b/packages/unified_iur/lib/unified_iur/fixtures.ex
index a80c32fe..dcd38c4a 100644
--- a/packages/unified_iur/lib/unified_iur/fixtures.ex
+++ b/packages/unified_iur/lib/unified_iur/fixtures.ex
@@ -1055,6 +1055,51 @@ defmodule UnifiedIUR.Fixtures do
hydrated?: true,
row_count: 0,
children: []
+ )},
+ {:content,
+ Components.mode_nav(
+ [
+ %{value: :workspace, label: "Workspace", current?: true},
+ %{value: :settings, label: "Settings"}
+ ],
+ id: "component-mode-nav",
+ aria_label: "Application modes",
+ navigation_intent: :switch_mode
+ )},
+ {:content, Components.unread_badge(5, id: "component-unread-badge", threshold: 99)},
+ {:content,
+ Components.top_strip(
+ [],
+ id: "component-top-strip",
+ brand: "Ariston",
+ context: "Workspace"
+ )},
+ {:content,
+ Components.sidebar_shell(
+ [
+ Components.sidebar_section(
+ "Docs",
+ [
+ Components.sidebar_item("Overview", [], id: "sidebar-item-overview"),
+ Components.sidebar_item("Details", [],
+ id: "sidebar-item-details",
+ selected?: true
+ )
+ ],
+ id: "sidebar-section-docs"
+ )
+ ],
+ id: "component-sidebar-shell"
+ )},
+ {:content,
+ Components.command_palette(
+ [
+ %{id: "cmd-1", label: "Open Spec"},
+ %{id: "cmd-2", label: "New Doc"}
+ ],
+ [],
+ id: "component-command-palette",
+ open?: false
)}
],
id: "component-safety-fixture"
diff --git a/packages/unified_iur/lib/unified_iur/widgets/components.ex b/packages/unified_iur/lib/unified_iur/widgets/components.ex
index bab658d8..983a4910 100644
--- a/packages/unified_iur/lib/unified_iur/widgets/components.ex
+++ b/packages/unified_iur/lib/unified_iur/widgets/components.ex
@@ -35,7 +35,8 @@ defmodule UnifiedIUR.Widgets.Components do
@form_control_kinds [
:segmented_button_group,
:runtime_form_shell,
- :chat_composer
+ :chat_composer,
+ :mode_nav
]
@row_artifact_kinds [
@@ -47,13 +48,19 @@ defmodule UnifiedIUR.Widgets.Components do
:pipeline_stepper_horizontal,
:segmented_progress_bar,
:workflow_stage_list_vertical,
- :meter_thin
+ :meter_thin,
+ :unread_badge
]
@layer_callout_kinds [
:sticky_frosted_header,
:slide_over_panel,
- :event_callout
+ :event_callout,
+ :top_strip,
+ :sidebar_shell,
+ :sidebar_section,
+ :sidebar_item,
+ :command_palette
]
@redline_code_kinds [
@@ -503,6 +510,158 @@ defmodule UnifiedIUR.Widgets.Components do
)
end
+ @spec top_strip(
+ [Element.t() | Element.Child.t() | {atom(), Element.t()} | map()],
+ opts()
+ ) :: Element.t()
+ def top_strip(children \\ [], opts \\ []) when is_list(children) do
+ opts = normalize_opts(opts)
+
+ build_component(
+ :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)
+ }
+ },
+ Map.put(opts, :children, children)
+ )
+ end
+
+ @spec mode_nav([keyword() | map()], opts()) :: Element.t()
+ def mode_nav(items, opts \\ []) when is_list(items) do
+ opts = normalize_opts(opts)
+
+ build_component(
+ :mode_nav,
+ :form_control_and_composer,
+ %{
+ navigation:
+ %{
+ items: normalize_maps(items)
+ }
+ |> maybe_put(:aria_label, option(opts, :aria_label))
+ |> maybe_put(:navigation_intent, option(opts, :navigation_intent))
+ |> maybe_put(:navigation_value_key, option(opts, :navigation_value_key))
+ },
+ opts
+ )
+ end
+
+ @spec sidebar_shell(
+ [Element.t() | Element.Child.t() | {atom(), Element.t()} | map()],
+ opts()
+ ) :: Element.t()
+ def sidebar_shell(children \\ [], opts \\ []) when is_list(children) do
+ opts = normalize_opts(opts)
+
+ build_component(
+ :sidebar_shell,
+ :layer_shell_and_callout,
+ %{
+ shell: %{
+ position: :side,
+ collapsed?: option(opts, :collapsed?, false)
+ }
+ },
+ Map.put(opts, :children, children)
+ )
+ end
+
+ @spec sidebar_section(
+ String.t(),
+ [Element.t() | Element.Child.t() | {atom(), Element.t()} | map()],
+ opts()
+ ) :: Element.t()
+ def sidebar_section(label, children \\ [], opts \\ [])
+ when is_binary(label) and is_list(children) do
+ opts = normalize_opts(opts)
+
+ build_component(
+ :sidebar_section,
+ :layer_shell_and_callout,
+ %{
+ section:
+ %{label: label}
+ |> maybe_put(:action_glyph, option(opts, :action_glyph))
+ |> maybe_put(:action_label, option(opts, :action_label))
+ |> maybe_put(:action_intent, option(opts, :action_intent))
+ },
+ Map.put(opts, :children, children)
+ )
+ end
+
+ @spec sidebar_item(
+ String.t(),
+ [Element.t() | Element.Child.t() | {atom(), Element.t()} | map()],
+ opts()
+ ) :: Element.t()
+ def sidebar_item(label, children \\ [], opts \\ [])
+ when is_binary(label) and is_list(children) do
+ opts = normalize_opts(opts)
+
+ build_component(
+ :sidebar_item,
+ :layer_shell_and_callout,
+ %{
+ item:
+ %{
+ label: label,
+ selected?: option(opts, :selected?, false)
+ }
+ |> maybe_put(:item_intent, option(opts, :item_intent))
+ },
+ Map.put(opts, :children, children)
+ )
+ end
+
+ @spec unread_badge(non_neg_integer(), opts()) :: Element.t()
+ def unread_badge(count, opts \\ []) when is_integer(count) and count >= 0 do
+ opts = normalize_opts(opts)
+
+ build_component(
+ :unread_badge,
+ :workflow_progress_and_status,
+ %{
+ status: %{
+ count: count,
+ threshold: option(opts, :threshold, 99)
+ }
+ },
+ opts
+ )
+ end
+
+ @spec command_palette(
+ [keyword() | map()],
+ [Element.t() | Element.Child.t() | {atom(), Element.t()} | map()],
+ opts()
+ ) :: Element.t()
+ def command_palette(items \\ [], children \\ [], opts \\ [])
+ when is_list(items) and is_list(children) do
+ opts = normalize_opts(opts)
+
+ build_component(
+ :command_palette,
+ :layer_shell_and_callout,
+ %{
+ palette:
+ %{
+ open?: option(opts, :open?, false),
+ items: normalize_maps(items)
+ }
+ |> maybe_put(:filter_intent, option(opts, :filter_intent))
+ |> maybe_put(:select_intent, option(opts, :select_intent))
+ },
+ Map.put(opts, :children, children)
+ )
+ end
+
defp build_component(kind, family, kind_attributes, opts) do
opts = normalize_opts(opts)
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 ea046a1d..fe650c6e 100644
--- a/packages/unified_iur/test/unified_iur/widgets/components_test.exs
+++ b/packages/unified_iur/test/unified_iur/widgets/components_test.exs
@@ -19,7 +19,8 @@ defmodule UnifiedIUR.Widgets.ComponentsTest do
assert Components.form_control_kinds() == [
:segmented_button_group,
:runtime_form_shell,
- :chat_composer
+ :chat_composer,
+ :mode_nav
]
assert Components.row_artifact_kinds() == [:list_item_multi_column, :artifact_row]
@@ -28,13 +29,19 @@ defmodule UnifiedIUR.Widgets.ComponentsTest do
:pipeline_stepper_horizontal,
:segmented_progress_bar,
:workflow_stage_list_vertical,
- :meter_thin
+ :meter_thin,
+ :unread_badge
]
assert Components.layer_callout_kinds() == [
:sticky_frosted_header,
:slide_over_panel,
- :event_callout
+ :event_callout,
+ :top_strip,
+ :sidebar_shell,
+ :sidebar_section,
+ :sidebar_item,
+ :command_palette
]
assert Components.redline_code_kinds() == [