diff --git a/lib/ash_ui/rendering/iur_adapter.ex b/lib/ash_ui/rendering/iur_adapter.ex
index 370f6977..74cd88e3 100644
--- a/lib/ash_ui/rendering/iur_adapter.ex
+++ b/lib/ash_ui/rendering/iur_adapter.ex
@@ -625,6 +625,42 @@ defmodule AshUI.Rendering.IURAdapter do
defp base_attributes(:canvas, props), do: %{canvas: Map.drop(props, attachment_prop_keys())}
defp base_attributes(:form_builder, props), do: %{form: Map.drop(props, attachment_prop_keys())}
defp base_attributes(:field_group, props), do: %{group: Map.drop(props, attachment_prop_keys())}
+
+ defp base_attributes(:needs_you_section = kind, props) do
+ component_attributes(
+ kind,
+ :workflow_progress_and_status,
+ %{
+ section:
+ compact_map(%{
+ title: first_present(props, [:title, :label]),
+ empty_state_text: first_present(props, [:empty_state_text]),
+ max_visible: fetch(props, :max_visible)
+ })
+ },
+ props
+ )
+ end
+
+ defp base_attributes(:blocker_row = kind, props) do
+ component_attributes(
+ kind,
+ :row_and_artifact,
+ %{
+ blocker:
+ compact_map(%{
+ row_id: first_present(props, [:row_id, :id]),
+ ask_text: first_present(props, [:ask_text, :text, :label]),
+ scope_label: first_present(props, [:scope_label, :scope]),
+ scope_intent: first_present(props, [:scope_intent]) || :jump_to_blocker,
+ severity: normalize_existing_atom(first_present(props, [:severity]) || :info)
+ }),
+ actor: normalize_map(fetch(props, :actor))
+ },
+ props
+ )
+ end
+
defp base_attributes(kind, props), do: %{kind => Map.drop(props, attachment_prop_keys())}
defp style_attributes(props) do
diff --git a/lib/ash_ui/rendering/live_ui_adapter.ex b/lib/ash_ui/rendering/live_ui_adapter.ex
index 7f4fbe0a..ff1a66be 100644
--- a/lib/ash_ui/rendering/live_ui_adapter.ex
+++ b/lib/ash_ui/rendering/live_ui_adapter.ex
@@ -549,6 +549,88 @@ defmodule AshUI.Rendering.LiveUIAdapter do
"""
end
+ defp generate_heex(%{"type" => "needs_you_section"} = iur, opts) do
+ props = iur["props"] || %{}
+ section = normalize_item(prop(props, "section", %{}))
+ title = escaped_text_prop(section, ["title", "label"], "Needs you")
+ empty_state_text = escaped_text_prop(section, "empty_state_text", "You're all caught up.")
+ max_visible = numeric_value(section, "max_visible", 5)
+ children = List.wrap(iur["children"] || [])
+
+ visible_children = Enum.take(children, max_visible)
+ overflow_count = length(children) - max_visible
+
+ rows_html = generate_children(visible_children, opts)
+
+ overflow_html =
+ if overflow_count > 0 do
+ ~s()
+ else
+ ""
+ end
+
+ item_count = length(children)
+
+ count_html =
+ if children != [] do
+ "(#{item_count})"
+ else
+ ""
+ end
+
+ list_or_empty_html =
+ if children == [] do
+ ~s(
#{empty_state_text}
)
+ else
+ ~s(#{overflow_html})
+ end
+
+ """
+
+
+ #{list_or_empty_html}
+
+ """
+ end
+
+ defp generate_heex(%{"type" => "blocker_row"} = iur, _opts) do
+ props = iur["props"] || %{}
+ blocker = normalize_item(prop(props, "blocker", %{}))
+ actor = normalize_item(prop(props, "actor", %{}))
+
+ ask_text = escaped_text_prop(blocker, ["ask_text", "text", "label"], "")
+ scope_label = escaped_text_prop(blocker, ["scope_label", "scope"], "")
+ severity = escaped_text_prop(blocker, "severity", "info")
+ row_id = html_attr(prop(blocker, "row_id", prop(iur, "id", "")))
+
+ actor_initials = escaped_text_prop(actor, ["initials"], "")
+ actor_name = escaped_text_prop(actor, ["actor_name", "name"], "")
+ actor_image = prop(actor, "image_source")
+
+ aria_label = "#{ask_text} — #{scope_label}"
+
+ avatar_html =
+ if actor_image do
+ ~s(
)
+ else
+ ~s(#{actor_initials})
+ end
+
+ """
+
+ """
+ end
+
defp generate_heex(%{"type" => "artifact_row"} = iur, opts) do
props = iur["props"] || %{}
title = escaped_text_prop(props, ["title", "label"], "Artifact")
diff --git a/packages/live_ui/lib/live_ui/renderer.ex b/packages/live_ui/lib/live_ui/renderer.ex
index 14f6072e..a6a81edf 100644
--- a/packages/live_ui/lib/live_ui/renderer.ex
+++ b/packages/live_ui/lib/live_ui/renderer.ex
@@ -239,6 +239,69 @@ defmodule LiveUi.Renderer do
"""
end
+ # NOTE: `:needs_you_section` and `:blocker_row` are members of `@component_kinds`
+ # (via `@workflow_kinds` and `@row_artifact_kinds`). Place these dedicated clauses
+ # BEFORE the generic `@component_kinds` fallback to avoid shadowing.
+ def render(%{element: %Element{kind: :needs_you_section}} = assigns) do
+ interaction_attrs = interaction_event_attrs(assigns.element, Map.get(assigns, :event_target))
+
+ assigns =
+ assign(assigns, :interaction_attrs, interaction_attrs)
+ |> assign(:style_attrs, merge_global_attrs(style_rest(assigns.element), interaction_attrs))
+
+ ~H"""
+
+ """
+ end
+
+ def render(%{element: %Element{kind: :blocker_row}} = assigns) do
+ interaction_attrs = interaction_event_attrs(assigns.element, Map.get(assigns, :event_target))
+
+ assigns =
+ assign(assigns, :interaction_attrs, interaction_attrs)
+ |> assign(:style_attrs, merge_global_attrs(style_rest(assigns.element), interaction_attrs))
+
+ # Use element id, falling back to a prefixed row_id so multiple rows in the
+ # same section don't collide on the LiveComponent id requirement.
+ row_id_attr = string_value(get_in(assigns.element.attributes, [:blocker, :row_id]), "row")
+ component_id = element_id(assigns.element, "blocker-row-#{row_id_attr}")
+ assigns = assign(assigns, :component_id, component_id)
+
+ ~H"""
+
+ """
+ end
+
# NOTE: `:command_palette` is a member of `@layer_callout_kinds` (and therefore
# of `@component_kinds`), so the generic fallback below would shadow any later
# `:command_palette` clause. Keep this specific clause BEFORE the generic
diff --git a/packages/live_ui/lib/live_ui/widgets.ex b/packages/live_ui/lib/live_ui/widgets.ex
index 6ef7843b..2923a177 100644
--- a/packages/live_ui/lib/live_ui/widgets.ex
+++ b/packages/live_ui/lib/live_ui/widgets.ex
@@ -13,19 +13,38 @@ defmodule LiveUi.Widgets do
| :data
| :operational
| :display
+ | :workflow
+ | :row_and_artifact
@type widget_module :: module()
@spec families() :: [family()]
def families do
- [:content, :input, :navigation, :feedback, :layout, :overlay, :data, :operational, :display]
+ [
+ :content,
+ :input,
+ :navigation,
+ :feedback,
+ :layout,
+ :overlay,
+ :data,
+ :operational,
+ :display,
+ :workflow,
+ :row_and_artifact
+ ]
end
@spec modules() :: [widget_module()]
def modules do
foundational_modules() ++
input_modules() ++
- navigation_modules() ++ advanced_modules() ++ overlay_modules() ++ display_modules()
+ navigation_modules() ++
+ advanced_modules() ++
+ overlay_modules() ++
+ display_modules() ++
+ workflow_modules() ++
+ row_and_artifact_modules()
end
@spec metadata() :: [LiveUi.Component.Metadata.t()]
@@ -63,6 +82,16 @@ defmodule LiveUi.Widgets do
LiveUi.Widgets.Display.modules()
end
+ @spec workflow_modules() :: [widget_module()]
+ def workflow_modules do
+ LiveUi.Widgets.Workflow.modules()
+ end
+
+ @spec row_and_artifact_modules() :: [widget_module()]
+ def row_and_artifact_modules do
+ LiveUi.Widgets.RowAndArtifact.modules()
+ end
+
@spec namespace() :: module()
def namespace, do: __MODULE__
end
diff --git a/packages/live_ui/lib/live_ui/widgets/blocker_row.ex b/packages/live_ui/lib/live_ui/widgets/blocker_row.ex
new file mode 100644
index 00000000..73da382b
--- /dev/null
+++ b/packages/live_ui/lib/live_ui/widgets/blocker_row.ex
@@ -0,0 +1,86 @@
+defmodule LiveUi.Widgets.BlockerRow do
+ @moduledoc """
+ Single actionable blocker row for use inside `needs_you_section`.
+
+ Renders as a `