From 9fd8cb9e1d3e3f82c839dfc6d95a93f42f397846 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 25 Apr 2026 23:21:25 +0200 Subject: [PATCH] fix: more work on liveview Co-authored-by: Copilot --- PLAN.md | 4 +- lib/bds/desktop/shell_data.ex | 6 +- lib/bds/desktop/shell_live.ex | 526 ++++++++------------- lib/bds/desktop/shell_live/index.html.heex | 348 ++++++++++++++ lib/bds/sidecar.ex | 2 +- priv/ui/app.css | 7 + test/bds/desktop/shell_live_test.exs | 17 + 7 files changed, 571 insertions(+), 339 deletions(-) create mode 100644 lib/bds/desktop/shell_live/index.html.heex diff --git a/PLAN.md b/PLAN.md index f2f5bb5..44d2aa4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -13,7 +13,7 @@ The rewrite already implements most of the backend and compatibility-critical su - Compatibility parity locks: executable parity coverage now pins the old-bDS behavior for serializer/file contracts, metadata-diff ordering, canonical generation routes, feed output, localized archive rendering, preview draft language selection, taxonomy archive links, and media URL rewriting. - Core editorial domains: projects, posts, post translations, media, media translations, tags, templates, scripts, menu data, and project metadata. - Output and infrastructure: search, Liquid rendering, site generation, preview server, publishing, background tasks, i18n, git support, MCP server, AI operations, embeddings, and CLI sync. -- Desktop shell foundation: native menu definitions, LiveView shell endpoint, activity bar, sidebar views, tab/workbench state, task panel, assistant sidebar, project switcher, and shell command routing. +- Desktop shell foundation: native menu definitions, LiveView shell endpoint, activity bar, sidebar views, tab/workbench state, task panel, assistant sidebar, status bar, project switcher, shell command routing, and template-backed shell rendering. ### Implemented But Not Yet At Parity @@ -53,7 +53,7 @@ The remaining work needs to proceed from base contracts upward. Later phases sho Save/publish/delete side-effects, published-post `templateSlug` frontmatter rewrites, manual-translation source-post reopening, post-to-media sidecar cleanup, auto-translation task cascades, linked-media translation cascades, link graph maintenance, thumbnail regeneration rules, and rebuild notifications are now implemented and covered at the backend layer independent of UI. 3. Finish the desktop shell primitives. Completed 2026-04-25. - Route state, registry-backed shell command coverage, panel fallback integration, and menu/native-command wiring now cover every sidebar view and singleton editor route while preserving the old app shell frame and styling. + Route state, registry-backed shell command coverage, panel fallback integration, menu/native-command wiring, template-backed LiveView rendering, and sidebar-to-tab/status-bar interactions now cover every sidebar view and singleton editor route while preserving the old app shell frame and styling. 4. Implement the shared modal and confirmation layer. Add the modal/overlay system required by the specs: AI suggestions, delete confirmations, merge confirmation, picker overlays, and gallery flows. diff --git a/lib/bds/desktop/shell_data.ex b/lib/bds/desktop/shell_data.ex index d10afff..8ffce8f 100644 --- a/lib/bds/desktop/shell_data.ex +++ b/lib/bds/desktop/shell_data.ex @@ -87,13 +87,13 @@ defmodule BDS.Desktop.ShellData do ] end - def status_bar(workbench, task_status, dashboard) do + def status_bar(workbench, task_status, dashboard, opts \\ []) do Workbench.status_bar(workbench, post_count: dashboard.post_stats.total_posts, media_count: dashboard.media_stats.media_count, theme_badge: "desktop-shell", - ui_language: ui_language(), - offline_mode: true, + ui_language: Keyword.get(opts, :ui_language, ui_language()), + offline_mode: Keyword.get(opts, :offline_mode, true), running_task_message: task_status.running_task_message, running_task_overflow: task_status.running_task_overflow, active_post_status: nil diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex index 93dcee8..f0fca01 100644 --- a/lib/bds/desktop/shell_live.ex +++ b/lib/bds/desktop/shell_live.ex @@ -6,10 +6,13 @@ defmodule BDS.Desktop.ShellLive do import Phoenix.HTML alias BDS.Desktop.ShellData + alias BDS.UI.Registry alias BDS.UI.Workbench @refresh_interval 1_500 + embed_templates "shell_live/*" + @impl true def mount(_params, _session, socket) do if connected?(socket) do @@ -22,6 +25,8 @@ defmodule BDS.Desktop.ShellLive do socket |> assign(:page_title, ShellData.title()) |> assign(:page_language, ShellData.ui_language()) + |> assign(:offline_mode, true) + |> assign(:tab_meta, %{}) |> reload_shell(workbench)} end @@ -52,6 +57,37 @@ defmodule BDS.Desktop.ShellLive do {:noreply, reload_shell(socket, workbench)} end + def handle_event("open_sidebar_item", %{"route" => route, "id" => id} = params, socket) do + route_atom = sidebar_route_atom(route) + tab_id = tab_id_for_route(route_atom, id) + + workbench = + Workbench.open_tab(socket.assigns.workbench, route_atom, tab_id, tab_intent(route_atom)) + + tab_meta = + Map.put(socket.assigns.tab_meta, {route_atom, tab_id}, %{ + title: Map.get(params, "title", ""), + subtitle: Map.get(params, "subtitle", "") + }) + + {:noreply, + socket + |> assign(:tab_meta, tab_meta) + |> reload_shell(workbench)} + end + + def handle_event("select_tab", %{"type" => type, "id" => id}, socket) do + workbench = + Workbench.open_tab(socket.assigns.workbench, String.to_existing_atom(type), id, :preview) + + {:noreply, reload_shell(socket, workbench)} + end + + def handle_event("toggle_offline_mode", _params, socket) do + socket = assign(socket, :offline_mode, not socket.assigns.offline_mode) + {:noreply, reload_shell(socket, socket.assigns.workbench)} + end + @impl true def handle_info(:refresh_task_status, socket) do task_status = BDS.Tasks.status_snapshot() @@ -60,312 +96,17 @@ defmodule BDS.Desktop.ShellLive do socket |> assign(:task_status, task_status) |> assign(:editor_meta, ShellData.editor_meta(task_status)) - |> assign(:status, ShellData.status_bar(socket.assigns.workbench, task_status, socket.assigns.dashboard))} + |> assign( + :status, + ShellData.status_bar(socket.assigns.workbench, task_status, socket.assigns.dashboard, + ui_language: socket.assigns.page_language, + offline_mode: socket.assigns.offline_mode + ) + )} end @impl true - def render(assigns) do - ~H""" -
-
- -
-
<%= @page_title %>
-
- - - -
-
- -
- - -
- - -
- -
-
-
-
-
-

<%= translated("dashboard.title") %>

-

<%= translated("dashboard.subtitle") %>

- -
-
-
<%= @dashboard.post_stats.total_posts || 0 %>
-
<%= translated("dashboard.stats.totalPosts") %>
-
- <%= translated("dashboard.stats.published", %{count: @dashboard.post_stats.published_count || 0}) %> - <%= translated("dashboard.stats.drafts", %{count: @dashboard.post_stats.draft_count || 0}) %> - <%= if (@dashboard.post_stats.archived_count || 0) > 0 do %> - <%= translated("dashboard.stats.archived", %{count: @dashboard.post_stats.archived_count || 0}) %> - <% end %> -
-
-
-
<%= @dashboard.media_stats.media_count || 0 %>
-
<%= translated("dashboard.stats.mediaFiles") %>
-
- <%= translated("dashboard.stats.images", %{count: @dashboard.media_stats.image_count || 0}) %> - <%= ShellData.format_bytes(@dashboard.media_stats.total_bytes || 0) %> -
-
-
-
<%= length(@dashboard.tag_cloud_items || []) %>
-
<%= translated("dashboard.stats.tags") %>
-
- <%= translated("dashboard.stats.categories", %{count: length(@dashboard.category_counts || [])}) %> -
-
-
- - <%= if Enum.any?(@dashboard.timeline_entries || []) do %> -
-

<%= translated("dashboard.section.postsOverTime") %>

-
- <%= for entry <- @dashboard.timeline_entries || [] do %> -
-
- <%= entry.count || 0 %> -
-
- <%= ShellData.format_dashboard_month(entry.year, entry.month) %> - <%= entry.year %> -
-
- <% end %> -
-
- <% end %> - - <%= if Enum.any?(@dashboard_tag_cloud_items) do %> -
-

<%= translated("dashboard.section.tags") %>

-
- <%= for item <- @dashboard_tag_cloud_items do %> - <%= item.tag %> - <% end %> - <%= if length(@dashboard.tag_cloud_items || []) > 40 do %> - <%= translated("dashboard.tagCloud.more", %{count: length(@dashboard.tag_cloud_items) - 40}) %> - <% end %> -
-
- <% end %> - - <%= if Enum.any?(@dashboard.category_counts || []) do %> -
-

<%= translated("dashboard.section.categories") %>

-
- <%= for category <- @dashboard.category_counts || [] do %> - - <%= category.category || "" %> - <%= category.count || 0 %> - - <% end %> -
-
- <% end %> - - <%= if Enum.any?(@dashboard.recent_posts || []) do %> -
-

<%= translated("dashboard.section.recentlyUpdated") %>

-
- <%= for post <- @dashboard.recent_posts || [] do %> - - <% end %> -
-
- <% end %> - - -
-
-
- -
-
-
- <%= for tab <- @panel_tabs do %> - - <% end %> -
-
-
- <%= render_panel_body(assigns) %> -
-
-
- -
-
- -
-
- -
-
-
- -
- -
-
- <%= @status.right.post_count %> - <%= @status.right.media_count %> - <%= @status.right.theme_badge %> - - - <%= @status.right.brand %> -
-
-
- """ - end + def render(assigns), do: index(assigns) defp reload_shell(socket, workbench) do projects = ShellData.project_snapshot() @@ -373,22 +114,34 @@ defmodule BDS.Desktop.ShellLive do sidebar_data = ShellData.sidebar_view(projects.active_project_id, Atom.to_string(workbench.active_view)) task_status = BDS.Tasks.status_snapshot() activity_buttons = Workbench.activity_buttons(workbench, 0) + page_language = socket.assigns[:page_language] || ShellData.ui_language() + offline_mode = Map.get(socket.assigns, :offline_mode, true) socket |> assign(:workbench, workbench) |> assign(:projects, projects) |> assign(:current_project, ShellData.current_project(projects)) |> assign(:dashboard, dashboard) - |> assign(:dashboard_tag_cloud_items, ShellData.dashboard_tag_cloud_items(dashboard.tag_cloud_items || [])) + |> assign(:dashboard_timeline_entries, Map.get(dashboard, :timeline_entries, [])) + |> assign(:dashboard_category_counts, Map.get(dashboard, :category_counts, [])) + |> assign(:dashboard_recent_posts, Map.get(dashboard, :recent_posts, [])) + |> assign(:dashboard_tag_cloud_items, ShellData.dashboard_tag_cloud_items(Map.get(dashboard, :tag_cloud_items, []))) |> assign(:sidebar_data, sidebar_data) |> assign(:sidebar_header, active_sidebar_label(activity_buttons, workbench.active_view, sidebar_data)) |> assign(:assistant_cards, ShellData.assistant_cards()) |> assign(:editor_meta, ShellData.editor_meta(task_status)) |> assign(:task_status, task_status) - |> assign(:status, ShellData.status_bar(workbench, task_status, dashboard)) + |> assign( + :status, + ShellData.status_bar(workbench, task_status, dashboard, + ui_language: page_language, + offline_mode: offline_mode + ) + ) |> assign(:activity_buttons, activity_buttons) |> assign(:panel_tabs, ShellData.panel_tabs(workbench)) |> assign(:supported_ui_languages, ShellData.supported_ui_languages()) + |> assign(:current_tab, current_tab(workbench)) end defp render_sidebar_body(assigns) do @@ -403,20 +156,31 @@ defmodule BDS.Desktop.ShellLive do defp render_post_sidebar(assigns) do ~H""" - <%= for section <- @sidebar_data.sections || [] do %> + <%= for section <- Map.get(@sidebar_data, :sections, []) do %> <% end %> - <%= if Enum.empty?(@sidebar_data.sections || []) do %> + <%= if Enum.empty?(Map.get(@sidebar_data, :sections, [])) do %> <% end %> """ @@ -435,10 +199,22 @@ defmodule BDS.Desktop.ShellLive do defp render_media_sidebar(assigns) do ~H""" - <%= if Enum.any?(@sidebar_data.items || []) do %> + <%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %> <% else %> <% end %> """ @@ -464,12 +240,23 @@ defmodule BDS.Desktop.ShellLive do defp render_entity_sidebar(assigns) do ~H""" - <%= if Enum.any?(@sidebar_data.items || []) do %> + <%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %>
- <%= for item <- @sidebar_data.items || [] do %> - @@ -477,7 +264,7 @@ defmodule BDS.Desktop.ShellLive do
<% else %> <% end %> """ @@ -486,10 +273,21 @@ defmodule BDS.Desktop.ShellLive do defp render_nav_sidebar(assigns) do ~H"""
- <%= for item <- @sidebar_data.items || [] do %> - <% end %>
@@ -498,13 +296,13 @@ defmodule BDS.Desktop.ShellLive do defp render_default_sidebar(assigns) do ~H""" - <%= for section <- @sidebar_data.sections || [] do %> + <%= for section <- Map.get(@sidebar_data, :sections, []) do %>