diff --git a/assets/css/utilities.css b/assets/css/utilities.css index 6ebb609..bfc27f9 100644 --- a/assets/css/utilities.css +++ b/assets/css/utilities.css @@ -1,4 +1,135 @@ @layer components { + .ui-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-height: 28px; + padding: 4px 10px; + border: 1px solid transparent; + border-radius: 4px; + font: inherit; + line-height: 1.2; + cursor: pointer; + user-select: none; + } + + .ui-button:hover:not(:disabled) { + background: var(--vscode-button-hoverBackground, #0e639c); + } + + .ui-button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .ui-button-primary { + color: var(--vscode-button-foreground, #ffffff); + background: var(--vscode-button-background, var(--vscode-focusBorder)); + } + + .ui-button-primary:hover:not(:disabled) { + background: var(--vscode-button-hoverBackground, #0e639c); + } + + .ui-button-secondary { + color: var(--vscode-button-secondaryForeground, var(--vscode-foreground)); + background: var(--vscode-button-secondaryBackground, rgba(255, 255, 255, 0.08)); + border-color: var(--vscode-button-border, transparent); + } + + .ui-button-secondary:hover:not(:disabled) { + background: var(--vscode-button-secondaryHoverBackground, #4a4d51); + } + + .ui-button-danger { + color: var(--vscode-errorForeground, #f48771); + background: transparent; + border-color: color-mix(in srgb, var(--vscode-errorForeground, #f48771) 45%, transparent); + } + + .ui-button-danger:hover:not(:disabled) { + background: color-mix(in srgb, var(--vscode-errorForeground, #f48771) 14%, transparent); + } + + .ui-button-compact { + min-height: 24px; + padding: 3px 8px; + font-size: 12px; + } + + .ui-input, + .ui-textarea { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--vscode-input-border, var(--vscode-panel-border)); + border-radius: 4px; + background: var(--vscode-input-background, rgba(255, 255, 255, 0.06)); + color: var(--vscode-input-foreground, var(--vscode-foreground)); + font: inherit; + } + + .ui-textarea { + line-height: 1.5; + resize: vertical; + } + + .ui-input:focus, + .ui-textarea:focus { + outline: 1px solid var(--vscode-focusBorder, #007fd4); + outline-offset: 1px; + } + + .ui-input-readonly, + .ui-input[readonly] { + opacity: 0.7; + cursor: not-allowed; + } + + .ui-input-disabled, + .ui-input:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .ui-tab { + border: none; + color: var(--vscode-tab-inactiveForeground, var(--vscode-foreground)); + background: transparent; + } + + .ui-tab:hover { + color: var(--vscode-tab-activeForeground, var(--vscode-foreground)); + } + + .ui-tab-active { + color: var(--vscode-tab-activeForeground, var(--vscode-foreground)); + } + + .ui-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; + } + + .ui-panel-entry { + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + background-color: var(--vscode-sideBar-background); + } + + .ui-empty-state { + display: flex; + flex-direction: column; + gap: 6px; + color: var(--vscode-descriptionForeground); + } + .btn-base { display: inline-flex; align-items: center; diff --git a/lib/bds/desktop/shell_live/chat_editor.ex b/lib/bds/desktop/shell_live/chat_editor.ex index 3e7bd09..256459b 100644 --- a/lib/bds/desktop/shell_live/chat_editor.ex +++ b/lib/bds/desktop/shell_live/chat_editor.ex @@ -16,6 +16,10 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()} @impl true + def update(%{action: :finish_request}, %{assigns: %{request: nil}} = socket) do + {:ok, socket} + end + def update(%{action: :finish_request, result: result}, socket) do {:ok, do_finish_request(socket, result)} end @@ -252,15 +256,28 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do notify_parent({:chat_editor_task_cancelled, conversation_id, ref}) - # Allow the terminated task's DB connection to be cleaned up before rebuilding. - Process.sleep(20) - socket |> assign(:request, nil) - |> build_data() + |> clear_streaming_state() end end + defp clear_streaming_state(socket) do + input = socket.assigns.input || "" + chat_editor = socket.assigns.chat_editor || %{} + + chat_editor = + chat_editor + |> Map.put(:is_streaming, false) + |> Map.put(:pending_user_message, nil) + |> Map.put(:streaming_content, "") + |> Map.put(:streaming_tool_markers, []) + |> Map.put(:streaming_inline_surfaces, []) + |> Map.put(:send_disabled?, String.trim(input) == "") + + assign(socket, :chat_editor, chat_editor) + end + defp do_finish_request(socket, result) do case result do {:ok, reply} -> @@ -314,7 +331,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do defp update_request(socket, updater) do case socket.assigns.request do nil -> - socket + build_data(socket) request -> socket diff --git a/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex b/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex index 49efdbb..bca18e6 100644 --- a/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex +++ b/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex @@ -12,7 +12,7 @@ <%= unless @chat_editor.needs_api_key? do %> + <% else %> @@ -80,18 +80,6 @@ <% else %> - <%= if @chat_editor.pending_user_message do %> -
-
👤
-
-
- <%= message_role_label(:user) %> -
-
<%= @chat_editor.pending_user_message %>
-
-
- <% end %> - <%= for message <- @chat_editor.messages do %>
<%= if message.role == :user, do: "👤", else: "🤖" %>
@@ -112,6 +100,18 @@ <% end %> + <%= if @chat_editor.pending_user_message do %> +
+
👤
+
+
+ <%= message_role_label(:user) %> +
+
<%= @chat_editor.pending_user_message %>
+
+
+ <% end %> + <%= if @chat_editor.is_streaming and (@chat_editor.streaming_content != "" or @chat_editor.streaming_tool_markers != []) do %>
🤖
@@ -149,12 +149,12 @@ <%= unless @chat_editor.needs_api_key? do %>
<%= if @chat_editor.is_streaming do %> - + <% end %>
- - + +
<%= if @chat_editor.action_error do %> diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex index e8e85d3..a743769 100644 --- a/lib/bds/desktop/shell_live/index.html.heex +++ b/lib/bds/desktop/shell_live/index.html.heex @@ -457,7 +457,7 @@
<%= for tab <- @panel_tabs do %>
- - - @@ -283,20 +283,20 @@
- +
- +
- +
diff --git a/lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex b/lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex index 593e39d..44c2a82 100644 --- a/lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex +++ b/lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex @@ -6,13 +6,13 @@
<%= if refreshable?(@misc_editor.kind) do %> - + <% end %> <%= if @misc_editor.kind == :site_validation do %> - + <% end %> <%= if @misc_editor.kind == :find_duplicates do %> - + <% end %>
@@ -59,7 +59,7 @@
<%= for tab <- @misc_editor.tabs do %> <% end %> @@ -95,7 +95,7 @@ <%= if @misc_editor.repair_enabled do %>
@@ -294,7 +294,7 @@ <%= if(BDS.MapUtils.attr(pair, :exact_match), do: dgettext("ui", "Exact Match"), else: "#{Float.round((BDS.MapUtils.attr(pair, :similarity) || 0.0) * 100, 1)}%") %> - + <% end %> @@ -306,7 +306,7 @@ <% else %>
- <%= for file_path <- @misc_editor.files do %> <% end %> diff --git a/lib/bds/desktop/shell_live/panel_renderer.ex b/lib/bds/desktop/shell_live/panel_renderer.ex index a137a5f..0136370 100644 --- a/lib/bds/desktop/shell_live/panel_renderer.ex +++ b/lib/bds/desktop/shell_live/panel_renderer.ex @@ -50,14 +50,14 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do defp render_task_entries(assigns) do ~H""" <%= if Enum.empty?(Map.get(@task_status, :tasks, [])) do %> -
+
<%= dgettext("ui", "Tasks") %> <%= dgettext("ui", "No background tasks running") %>
<% else %>
<%= for task <- Map.get(@task_status, :tasks, []) do %> -
+
<%= task.name %> <%= Map.get(task, :status_label, task.status |> to_string() |> String.capitalize()) %> @@ -79,7 +79,7 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do defp render_output_entries(assigns) do ~H""" <%= if Enum.empty?(@output_entries) do %> -
+
<%= dgettext("ui", "Output") %> <%= dgettext("ui", "No shell output yet") %>
@@ -88,6 +88,7 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do <%= for entry <- @output_entries do %>
@@ -113,17 +114,17 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do ~H""" <%= if Enum.empty?(@backlinks) and Enum.empty?(@outlinks) do %> -
+
<%= dgettext("ui", "Post Links") %> <%= dgettext("ui", "No post links yet") %>
<% else %>
<%= if Enum.any?(@backlinks) do %> -
<%= dgettext("ui", "Backlinks") %>
+
<%= dgettext("ui", "Backlinks") %>
<%= for entry <- @backlinks do %> <% end %> <%= if @post_editor.can_publish? do %> - <% end %> <%= if @post_editor.can_delete? do %> - <% end %> @@ -115,7 +115,7 @@
- +
@@ -164,20 +164,20 @@
- +
- <%= for language <- @post_editor.languages do %> <% end %> + <% end %> - - - - + + + +
-
-
+
+
-
-
+
+
diff --git a/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex b/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex index 7fdcb4d..090dbf9 100644 --- a/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex +++ b/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex @@ -10,7 +10,7 @@

<%= dgettext("ui", "Settings") %>

- +
@@ -32,29 +32,29 @@
-
+
-
+
- - + +
-
+
- <%= for language <- @settings_editor.supported_languages do %> <% end %> @@ -76,16 +76,16 @@
-
+
-
+
- <%= for category <- Enum.map(@settings_editor.categories, & &1.name) do %> <% end %> @@ -97,7 +97,7 @@

<%= dgettext("ui", "Bookmarklet copy support is wired through the desktop runtime and project public URL.") %>

-
+
<% end %> @@ -111,7 +111,7 @@
- @@ -121,7 +121,7 @@
- @@ -136,7 +136,7 @@
-
+
<% end %> @@ -161,12 +161,12 @@ <%= category.name %> - + - <%= for template <- @settings_editor.template_options.post do %> @@ -174,7 +174,7 @@ - <%= for template <- @settings_editor.template_options.list do %> @@ -186,8 +186,8 @@
- - + +
@@ -198,12 +198,12 @@
- - + +
-
+
<% end %> @@ -216,18 +216,18 @@
- - + +
-
+
-
+
@@ -239,11 +239,11 @@
-
+
-
+
@@ -253,14 +253,14 @@
- - + +
-
+
@@ -268,7 +268,7 @@
-
+
@@ -280,11 +280,11 @@
-
+
-
+
@@ -292,7 +292,7 @@
-
+
<%= for model <- @settings_editor.online_endpoint_models do %> @@ -305,7 +305,7 @@ <% end %> -
+
<% end %> @@ -322,7 +322,7 @@

<%= dgettext("ui", "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.") %>

-
+
<% end %> @@ -330,12 +330,12 @@

<%= dgettext("ui", "Publishing") %>

<%= dgettext("ui", "Deployment credentials for upload tasks") %>

-
-
-
-
+
+
+
+
-
+
<% end %> @@ -350,7 +350,7 @@

<%= agent.config_path || dgettext("ui", "Not supported in the rewrite yet") %>

-
@@ -364,14 +364,14 @@

<%= dgettext("ui", "Data Maintenance") %>

<%= dgettext("ui", "Rebuild filesystem-backed records and thumbnails") %>

- - - - - - - - + + + + + + + +
<% end %> diff --git a/lib/bds/desktop/shell_live/settings_editor_html/style_editor.html.heex b/lib/bds/desktop/shell_live/settings_editor_html/style_editor.html.heex index bcd7fa4..228c2f3 100644 --- a/lib/bds/desktop/shell_live/settings_editor_html/style_editor.html.heex +++ b/lib/bds/desktop/shell_live/settings_editor_html/style_editor.html.heex @@ -22,13 +22,13 @@
- +
diff --git a/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex b/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex index 4f150c1..8c861f3 100644 --- a/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex +++ b/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex @@ -16,9 +16,9 @@

<%= dgettext("ui", "Tag Cloud") %>

<%= if Enum.empty?(@tags_editor.tags) do %> -
+

<%= dgettext("ui", "No tags found") %>

- +
<% else %>
@@ -37,25 +37,25 @@
- + - +
<%= if @tags_editor.edit_draft != %{} do %>
- + - <%= for template <- @tags_editor.templates do %> <% end %> - - + +
<% end %> @@ -67,12 +67,12 @@
- <%= for tag_name <- @tags_editor.selected do %> <% end %> - +
@@ -81,7 +81,7 @@

<%= dgettext("ui", "Sync") %>

- +
diff --git a/lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex b/lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex index 872b00d..431ccf8 100644 --- a/lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex +++ b/lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex @@ -1,28 +1,29 @@
-
<%= @template_editor.title %>
+
<%= @template_editor.title %>
<%= BDS.Desktop.ShellData.dashboard_status_label(@template_editor.status) %> <%= if @template_editor.can_publish? do %> - + <% end %> - - - + + +
-
-
+
+
-
+
diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index 78a79ef..21677c9 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -205,15 +205,28 @@ defmodule BDS.Desktop.ShellLiveTest do settings_editor: %{ selected_section: "project", search_query: "", - active_sections: [], - project_visible?: false, + active_sections: ["project"], + project_visible?: true, editor_visible?: false, content_visible?: false, ai_visible?: false, publishing_visible?: false, data_visible?: false, technology_visible?: false, - mcp_visible?: false + mcp_visible?: false, + project: %{ + "name" => "Shell Project", + "description" => "Project settings", + "public_url" => "https://example.test", + "main_language" => "en", + "blog_languages" => ["en", "fr"], + "default_author" => "Author", + "max_posts_per_page" => 10, + "blogmark_category" => "notes" + }, + project_data_path: "/tmp/shell-project", + supported_languages: ["en", "fr"], + categories: [%{name: "notes"}, %{name: "posts"}] } } end @@ -225,10 +238,10 @@ defmodule BDS.Desktop.ShellLiveTest do selected_section: "cloud", tags: [], new_tag: %{"name" => "", "color" => "#3b82f6"}, - edit_draft: %{}, - selected: [], - merge_target: nil, - templates: [] + edit_draft: %{"name" => "news", "color" => "#3b82f6", "post_template_slug" => ""}, + selected: ["news", "updates"], + merge_target: "news", + templates: [%{slug: "post-template", title: "Post Template"}] } } end @@ -306,6 +319,64 @@ defmodule BDS.Desktop.ShellLiveTest do assert tags_html =~ "tag-form-row flex flex-wrap items-center gap-3" end + @tag :phase4 + test "phase 4 shared primitives render normalized classes" do + conn = Plug.Conn.put_private(build_conn(), :phoenix_endpoint, BDS.Desktop.Endpoint) + {:ok, view, _shell_html} = live_isolated(conn, BDS.Desktop.ShellLive) + + post_html = render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns()) + media_html = render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns()) + script_html = render_component(&BDS.Desktop.ShellLive.ScriptEditor.render/1, phase3_script_editor_assigns()) + template_html = render_component(&BDS.Desktop.ShellLive.TemplateEditor.render/1, phase3_template_editor_assigns()) + settings_html = render_component(&BDS.Desktop.ShellLive.SettingsEditor.render/1, phase3_settings_editor_assigns()) + tags_html = render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns()) + + panel_html = + render_component(&BDS.Desktop.ShellLive.PanelRenderer.render_panel_body/1, %{ + current_tab: %{type: :dashboard, id: "dashboard"}, + task_status: %{tasks: []}, + output_entries: [], + workbench: %{panel: %{active_tab: :tasks}} + }) + + assert post_html =~ ~s(class="status-badge ui-badge) + assert post_html =~ ~s(class="success ui-button ui-button-primary) + assert post_html =~ ~s(class="secondary danger ui-button ui-button-secondary ui-button-danger) + assert post_html =~ ~s(class="post-editor-input ui-input) + assert post_html =~ ~s(class="post-editor-textarea post-editor-excerpt ui-textarea) + assert post_html =~ ~s(class="editor-tab ui-tab ui-tab-active) + + assert media_html =~ ~s(class="secondary quick-actions-btn ui-button ui-button-secondary) + assert media_html =~ ~s(class="post-editor-input ui-input disabled ui-input-disabled) + assert media_html =~ ~s(class="post-editor-textarea ui-textarea) + + assert script_html =~ ~s(class="secondary scripts-save-button ui-button ui-button-secondary) + assert script_html =~ ~s(class="status-badge ui-badge) + assert script_html =~ ~s(class="ui-input") + + assert template_html =~ ~s(class="secondary templates-save-button ui-button ui-button-secondary) + assert template_html =~ ~s(class="status-badge ui-badge) + assert template_html =~ ~s(class="ui-input") + + assert settings_html =~ ~s(class="ui-input") + assert settings_html =~ ~s(class="primary ui-button ui-button-primary") + assert settings_html =~ ~s(class="secondary ui-button ui-button-secondary") + + assert tags_html =~ ~s(class="tags-empty-state ui-empty-state flex flex-col gap-3") + assert tags_html =~ ~s(class="secondary ui-button ui-button-secondary") + assert tags_html =~ ~s(class="primary ui-button ui-button-primary") + assert tags_html =~ ~s(class="danger ui-button ui-button-danger") + assert tags_html =~ ~s(class="ui-input") + + shell_html = + view + |> element("[data-testid='toggle-panel']") + |> render_click() + + assert shell_html =~ ~s(class="panel-tab ui-tab ui-tab-active) + assert panel_html =~ ~s(class="panel-entry ui-panel-entry panel-empty-state ui-empty-state) + end + alias BDS.Persistence alias BDS.AI alias BDS.CliSync.Watcher @@ -2025,12 +2096,12 @@ defmodule BDS.Desktop.ShellLiveTest do refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden") assert Regex.match?( - ~r/