fix: more work on liveview

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-25 23:21:25 +02:00
parent 2f34040aed
commit 9fd8cb9e1d
7 changed files with 571 additions and 339 deletions

View File

@@ -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. - 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. - 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. - 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 ### 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. 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. 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. 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. Add the modal/overlay system required by the specs: AI suggestions, delete confirmations, merge confirmation, picker overlays, and gallery flows.

View File

@@ -87,13 +87,13 @@ defmodule BDS.Desktop.ShellData do
] ]
end end
def status_bar(workbench, task_status, dashboard) do def status_bar(workbench, task_status, dashboard, opts \\ []) do
Workbench.status_bar(workbench, Workbench.status_bar(workbench,
post_count: dashboard.post_stats.total_posts, post_count: dashboard.post_stats.total_posts,
media_count: dashboard.media_stats.media_count, media_count: dashboard.media_stats.media_count,
theme_badge: "desktop-shell", theme_badge: "desktop-shell",
ui_language: ui_language(), ui_language: Keyword.get(opts, :ui_language, ui_language()),
offline_mode: true, offline_mode: Keyword.get(opts, :offline_mode, true),
running_task_message: task_status.running_task_message, running_task_message: task_status.running_task_message,
running_task_overflow: task_status.running_task_overflow, running_task_overflow: task_status.running_task_overflow,
active_post_status: nil active_post_status: nil

View File

@@ -6,10 +6,13 @@ defmodule BDS.Desktop.ShellLive do
import Phoenix.HTML import Phoenix.HTML
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.UI.Registry
alias BDS.UI.Workbench alias BDS.UI.Workbench
@refresh_interval 1_500 @refresh_interval 1_500
embed_templates "shell_live/*"
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
if connected?(socket) do if connected?(socket) do
@@ -22,6 +25,8 @@ defmodule BDS.Desktop.ShellLive do
socket socket
|> assign(:page_title, ShellData.title()) |> assign(:page_title, ShellData.title())
|> assign(:page_language, ShellData.ui_language()) |> assign(:page_language, ShellData.ui_language())
|> assign(:offline_mode, true)
|> assign(:tab_meta, %{})
|> reload_shell(workbench)} |> reload_shell(workbench)}
end end
@@ -52,6 +57,37 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, reload_shell(socket, workbench)} {:noreply, reload_shell(socket, workbench)}
end 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 @impl true
def handle_info(:refresh_task_status, socket) do def handle_info(:refresh_task_status, socket) do
task_status = BDS.Tasks.status_snapshot() task_status = BDS.Tasks.status_snapshot()
@@ -60,312 +96,17 @@ defmodule BDS.Desktop.ShellLive do
socket socket
|> assign(:task_status, task_status) |> assign(:task_status, task_status)
|> assign(:editor_meta, ShellData.editor_meta(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 end
@impl true @impl true
def render(assigns) do def render(assigns), do: index(assigns)
~H"""
<div class="app" id="bds-shell-app">
<div class="window-titlebar" data-region="title-bar">
<div class="window-titlebar-menu-bar is-hidden">
<button class="window-titlebar-menu-button" type="button">File</button>
<button class="window-titlebar-menu-button" type="button">Edit</button>
<button class="window-titlebar-menu-button" type="button">View</button>
<button class="window-titlebar-menu-button" type="button">Blog</button>
<button class="window-titlebar-menu-button" type="button">Help</button>
</div>
<div class="window-titlebar-drag-region"></div>
<div class="window-titlebar-title" data-testid="window-title"><%= @page_title %></div>
<div class="window-titlebar-actions">
<button
class="window-titlebar-action-button"
data-testid="toggle-sidebar"
type="button"
phx-click="toggle_sidebar"
aria-label="Toggle sidebar"
title="Toggle sidebar"
>
<span class={["window-titlebar-sidebar-icon", if(@workbench.sidebar_visible, do: "is-active", else: "is-inactive")]}>
<span class="window-titlebar-sidebar-pane"></span>
</span>
</button>
<button
class="window-titlebar-action-button"
data-testid="toggle-panel"
type="button"
phx-click="toggle_panel"
aria-label="Toggle panel"
title="Toggle panel"
>
<span class={["window-titlebar-panel-icon", if(@workbench.panel.visible, do: "is-active", else: "is-inactive")]}>
<span class="window-titlebar-panel-pane"></span>
</span>
</button>
<button
class="window-titlebar-action-button"
data-testid="toggle-assistant"
type="button"
phx-click="toggle_assistant_sidebar"
aria-label="Toggle assistant"
title="Toggle assistant"
>
<span class={["window-titlebar-assistant-icon", if(@workbench.assistant_sidebar_visible, do: "is-active", else: "is-inactive")]}>
<span class="window-titlebar-assistant-pane"></span>
</span>
</button>
</div>
</div>
<div class="app-main">
<aside class="activity-bar" data-region="activity-bar">
<div class="activity-bar-top">
<%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :top)) do %>
<button
class={["activity-bar-item", if(button.active, do: "active")]}
data-testid="activity-button"
data-view={button.id}
data-active={to_string(button.active)}
type="button"
phx-click="select_view"
phx-value-view={button.id}
title={activity_label(button.label)}
aria-label={activity_label(button.label)}
>
<%= raw(ShellData.activity_icon(button.id)) %>
</button>
<% end %>
</div>
<div class="activity-bar-bottom">
<%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :bottom)) do %>
<button
class={["activity-bar-item", if(button.active, do: "active")]}
data-testid="activity-button"
data-view={button.id}
data-active={to_string(button.active)}
type="button"
phx-click="select_view"
phx-value-view={button.id}
title={activity_label(button.label)}
aria-label={activity_label(button.label)}
>
<%= raw(ShellData.activity_icon(button.id)) %>
</button>
<% end %>
</div>
</aside>
<section
class={["sidebar-shell", if(not @workbench.sidebar_visible, do: "is-hidden")]}
data-testid="sidebar-shell"
style={"width: #{@workbench.sidebar_width}px;"}
>
<div class="sidebar" data-region="sidebar">
<div class="sidebar-content sidebar-body">
<div class="sidebar-section">
<div class="sidebar-section-header">
<span><%= String.upcase(sidebar_header_label(@sidebar_header)) %></span>
</div>
</div>
<%= render_sidebar_body(assigns) %>
</div>
</div>
<div class="resizable-panel-divider sidebar-divider" data-resize="sidebar" data-role="resize-handle"></div>
</section>
<main class="app-content" data-region="content">
<div class="tab-bar" data-region="tab-bar"></div>
<section class="editor-shell" data-region="editor">
<div class="editor-empty">
<div class="dashboard-content">
<h1 data-testid="editor-title"><%= translated("dashboard.title") %></h1>
<p class="text-muted"><%= translated("dashboard.subtitle") %></p>
<div class="dashboard-stats">
<div class="stat-card">
<div class="stat-number"><%= @dashboard.post_stats.total_posts || 0 %></div>
<div class="stat-label"><%= translated("dashboard.stats.totalPosts") %></div>
<div class="stat-breakdown">
<span class="stat-tag stat-published"><%= translated("dashboard.stats.published", %{count: @dashboard.post_stats.published_count || 0}) %></span>
<span class="stat-tag stat-draft"><%= translated("dashboard.stats.drafts", %{count: @dashboard.post_stats.draft_count || 0}) %></span>
<%= if (@dashboard.post_stats.archived_count || 0) > 0 do %>
<span class="stat-tag stat-archived"><%= translated("dashboard.stats.archived", %{count: @dashboard.post_stats.archived_count || 0}) %></span>
<% end %>
</div>
</div>
<div class="stat-card">
<div class="stat-number"><%= @dashboard.media_stats.media_count || 0 %></div>
<div class="stat-label"><%= translated("dashboard.stats.mediaFiles") %></div>
<div class="stat-breakdown">
<span class="stat-tag"><%= translated("dashboard.stats.images", %{count: @dashboard.media_stats.image_count || 0}) %></span>
<span class="stat-tag"><%= ShellData.format_bytes(@dashboard.media_stats.total_bytes || 0) %></span>
</div>
</div>
<div class="stat-card">
<div class="stat-number"><%= length(@dashboard.tag_cloud_items || []) %></div>
<div class="stat-label"><%= translated("dashboard.stats.tags") %></div>
<div class="stat-breakdown">
<span class="stat-tag"><%= translated("dashboard.stats.categories", %{count: length(@dashboard.category_counts || [])}) %></span>
</div>
</div>
</div>
<%= if Enum.any?(@dashboard.timeline_entries || []) do %>
<div class="dashboard-section">
<h4><%= translated("dashboard.section.postsOverTime") %></h4>
<div class="timeline-chart">
<%= for entry <- @dashboard.timeline_entries || [] do %>
<div class="timeline-bar-container">
<div class="timeline-bar" style={"height: #{timeline_height(entry, @dashboard.timeline_entries || [])}%"}>
<span class="timeline-bar-count"><%= entry.count || 0 %></span>
</div>
<div class="timeline-bar-label">
<span class="timeline-bar-label-month"><%= ShellData.format_dashboard_month(entry.year, entry.month) %></span>
<span class="timeline-bar-label-year"><%= entry.year %></span>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<%= if Enum.any?(@dashboard_tag_cloud_items) do %>
<div class="dashboard-section">
<h4><%= translated("dashboard.section.tags") %></h4>
<div class="tag-cloud">
<%= for item <- @dashboard_tag_cloud_items do %>
<span class={["dashboard-tag", if(item.color, do: "has-color")]} style={ShellData.render_dashboard_tag_style(item)} title={ShellData.dashboard_post_count_label(item.count)}><%= item.tag %></span>
<% end %>
<%= if length(@dashboard.tag_cloud_items || []) > 40 do %>
<span class="text-muted tag-cloud-more"><%= translated("dashboard.tagCloud.more", %{count: length(@dashboard.tag_cloud_items) - 40}) %></span>
<% end %>
</div>
</div>
<% end %>
<%= if Enum.any?(@dashboard.category_counts || []) do %>
<div class="dashboard-section">
<h4><%= translated("dashboard.section.categories") %></h4>
<div class="tag-cloud">
<%= for category <- @dashboard.category_counts || [] do %>
<span class="dashboard-tag dashboard-category" title={ShellData.dashboard_post_count_label(category.count || 0)}>
<%= category.category || "" %>
<span class="tag-count"><%= category.count || 0 %></span>
</span>
<% end %>
</div>
</div>
<% end %>
<%= if Enum.any?(@dashboard.recent_posts || []) do %>
<div class="dashboard-section">
<h4><%= translated("dashboard.section.recentlyUpdated") %></h4>
<div class="recent-posts-list">
<%= for post <- @dashboard.recent_posts || [] do %>
<button class="recent-post-item" type="button">
<span class="recent-post-title"><%= post.title || "" %></span>
<span class={"recent-post-status status-#{post.status || "draft"}"}><%= ShellData.dashboard_status_label(post.status || "draft") %></span>
<span class="recent-post-date"><%= ShellData.format_dashboard_date(post.updated_at) %></span>
</button>
<% end %>
</div>
</div>
<% end %>
<div class="dashboard-inspector-meta" hidden>
<%= for item <- @editor_meta do %>
<section class="editor-meta-row">
<strong data-testid="editor-meta-label"><%= translated(item.label) %></strong>
<span><%= translated(item.value) %></span>
</section>
<% end %>
</div>
</div>
</div>
</section>
<section class={["panel-shell", if(not @workbench.panel.visible, do: "is-hidden")]} data-region="panel">
<div class="panel-header">
<div class="panel-tabs">
<%= for tab <- @panel_tabs do %>
<button
class={["panel-tab", if(@workbench.panel.active_tab == tab, do: "active")]}
type="button"
phx-click="select_panel_tab"
phx-value-tab={tab}
>
<%= panel_tab_label(tab) %>
</button>
<% end %>
</div>
</div>
<div class="panel-content">
<%= render_panel_body(assigns) %>
</div>
</section>
</main>
<section
class={["assistant-sidebar-shell", if(not @workbench.assistant_sidebar_visible, do: "is-hidden")]}
data-testid="assistant-shell"
style={"width: #{@workbench.assistant_sidebar_width}px;"}
>
<div class="resizable-panel-divider assistant-divider" data-resize="assistant" data-role="resize-handle"></div>
<aside class="assistant-sidebar" data-region="assistant-sidebar">
<div class="assistant-header">
<strong><%= translated("Assistant") %></strong>
</div>
<div class="assistant-content">
<%= for card <- @assistant_cards do %>
<section class="assistant-card">
<strong><%= translated(card.label) %></strong>
<span><%= translated(card.text) %></span>
</section>
<% end %>
</div>
</aside>
</section>
</div>
<footer class="status-bar" data-region="status-bar">
<div class="status-bar-left">
<div class="project-selector">
<button class="project-selector-trigger" type="button" title={translated("Switch project")}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="project-icon">
<path d="M14.5 3H7.71l-.85-.85A.5.5 0 0 0 6.5 2h-5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-10a.5.5 0 0 0-.5-.5zm-13 1h5.29l.85.85c.1.1.23.15.36.15h6.5v9h-13V4z"></path>
</svg>
<span class="project-name"><%= @current_project && @current_project.name || "My Blog" %></span>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" class="dropdown-arrow">
<path d="M4.5 5.5L8 9l3.5-3.5h-7z"></path>
</svg>
</button>
</div>
<button class="status-bar-item status-bar-task-button" type="button" phx-click="toggle_panel">
<span><%= @status.left.running_task_message || translated("Idle") %></span>
<%= if (@status.left.running_task_overflow || 0) > 0 do %>
<span class="status-bar-count">+<%= @status.left.running_task_overflow %></span>
<% end %>
</button>
</div>
<div class="status-bar-right">
<span class="status-bar-item"><%= @status.right.post_count %></span>
<span class="status-bar-item"><%= @status.right.media_count %></span>
<span class="status-bar-item theme-badge"><%= @status.right.theme_badge %></span>
<button class={["status-bar-item", "offline-badge", if(@status.right.offline_mode, do: "active")]} type="button" title={translated("Toggle offline mode")}>✈</button>
<label class="status-bar-item language-badge">
<span><%= translated("UI") %></span>
<select class="status-bar-language-select">
<%= for language <- @supported_ui_languages do %>
<option selected={language.code == @page_language} value={language.code}><%= language.flag %></option>
<% end %>
</select>
</label>
<span class="status-bar-item brand"><%= @status.right.brand %></span>
</div>
</footer>
</div>
"""
end
defp reload_shell(socket, workbench) do defp reload_shell(socket, workbench) do
projects = ShellData.project_snapshot() 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)) sidebar_data = ShellData.sidebar_view(projects.active_project_id, Atom.to_string(workbench.active_view))
task_status = BDS.Tasks.status_snapshot() task_status = BDS.Tasks.status_snapshot()
activity_buttons = Workbench.activity_buttons(workbench, 0) 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 socket
|> assign(:workbench, workbench) |> assign(:workbench, workbench)
|> assign(:projects, projects) |> assign(:projects, projects)
|> assign(:current_project, ShellData.current_project(projects)) |> assign(:current_project, ShellData.current_project(projects))
|> assign(:dashboard, dashboard) |> 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_data, sidebar_data)
|> assign(:sidebar_header, active_sidebar_label(activity_buttons, workbench.active_view, sidebar_data)) |> assign(:sidebar_header, active_sidebar_label(activity_buttons, workbench.active_view, sidebar_data))
|> assign(:assistant_cards, ShellData.assistant_cards()) |> assign(:assistant_cards, ShellData.assistant_cards())
|> assign(:editor_meta, ShellData.editor_meta(task_status)) |> assign(:editor_meta, ShellData.editor_meta(task_status))
|> assign(:task_status, 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(:activity_buttons, activity_buttons)
|> assign(:panel_tabs, ShellData.panel_tabs(workbench)) |> assign(:panel_tabs, ShellData.panel_tabs(workbench))
|> assign(:supported_ui_languages, ShellData.supported_ui_languages()) |> assign(:supported_ui_languages, ShellData.supported_ui_languages())
|> assign(:current_tab, current_tab(workbench))
end end
defp render_sidebar_body(assigns) do defp render_sidebar_body(assigns) do
@@ -403,20 +156,31 @@ defmodule BDS.Desktop.ShellLive do
defp render_post_sidebar(assigns) do defp render_post_sidebar(assigns) do
~H""" ~H"""
<%= for section <- @sidebar_data.sections || [] do %> <%= for section <- Map.get(@sidebar_data, :sections, []) do %>
<section class="sidebar-section"> <section class="sidebar-section">
<div class="sidebar-section-title"> <div class="sidebar-section-title">
<span class={"section-icon status-#{section.status || "draft"}"}>●</span> <span class={"section-icon status-#{Map.get(section, :status, "draft")}"}>●</span>
<span data-testid="sidebar-section-title"><%= translated(section.title) %></span> <span data-testid="sidebar-section-title"><%= translated(section.title) %></span>
<span class="sidebar-section-count"><%= section.count || length(section.items || []) %></span> <span class="sidebar-section-count"><%= Map.get(section, :count, length(Map.get(section, :items, []))) %></span>
</div> </div>
<div class="sidebar-list"> <div class="sidebar-list">
<%= for item <- section.items || [] do %> <%= for item <- Map.get(section, :items, []) do %>
<button class="sidebar-item sidebar-post-item post-type-post" type="button"> <button
class={["sidebar-item", "sidebar-post-item", "post-type-post", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "selected")]}
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
type="button"
phx-click="open_sidebar_item"
phx-value-route={item.route}
phx-value-id={item.id}
phx-value-title={item.title}
phx-value-subtitle={format_sidebar_timestamp(item.meta_timestamp)}
>
<span class="post-type-icon" title="post">●</span> <span class="post-type-icon" title="post">●</span>
<span class="sidebar-item-content"> <span class="sidebar-item-content">
<span class="sidebar-item-title-row"> <span class="sidebar-item-title-row">
<span class="sidebar-item-title"><%= item.title || "" %></span> <span class="sidebar-item-title"><%= item.title %></span>
</span> </span>
<span class="sidebar-item-meta"><%= format_sidebar_timestamp(item.meta_timestamp) %></span> <span class="sidebar-item-meta"><%= format_sidebar_timestamp(item.meta_timestamp) %></span>
</span> </span>
@@ -425,9 +189,9 @@ defmodule BDS.Desktop.ShellLive do
</div> </div>
</section> </section>
<% end %> <% end %>
<%= if Enum.empty?(@sidebar_data.sections || []) do %> <%= if Enum.empty?(Map.get(@sidebar_data, :sections, [])) do %>
<div class="sidebar-empty"> <div class="sidebar-empty">
<p><%= translated(@sidebar_data.empty_message || "No items") %></p> <p><%= translated(Map.get(@sidebar_data, :empty_message, "No items")) %></p>
</div> </div>
<% end %> <% end %>
""" """
@@ -435,10 +199,22 @@ defmodule BDS.Desktop.ShellLive do
defp render_media_sidebar(assigns) do defp render_media_sidebar(assigns) do
~H""" ~H"""
<%= if Enum.any?(@sidebar_data.items || []) do %> <%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %>
<div class="sidebar-list media-grid"> <div class="sidebar-list media-grid">
<%= for item <- @sidebar_data.items || [] do %> <%= for item <- Map.get(@sidebar_data, :items, []) do %>
<button class="media-item" type="button" title={item.title || ""}> <button
class={["media-item", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "selected")]}
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
type="button"
title={item.title}
phx-click="open_sidebar_item"
phx-value-route={item.route}
phx-value-id={item.id}
phx-value-title={item.title}
phx-value-subtitle={item.meta}
>
<span class={media_thumbnail_class(item)}> <span class={media_thumbnail_class(item)}>
<%= if image_media?(item) do %> <%= if image_media?(item) do %>
<span class="media-thumbnail-fallback"><%= media_thumbnail_glyph(item.mime_type) %></span> <span class="media-thumbnail-fallback"><%= media_thumbnail_glyph(item.mime_type) %></span>
@@ -448,15 +224,15 @@ defmodule BDS.Desktop.ShellLive do
<% end %> <% end %>
</span> </span>
<span class="media-item-info"> <span class="media-item-info">
<span class="media-item-name"><%= item.title || "" %></span> <span class="media-item-name"><%= item.title %></span>
<span class="media-item-size"><%= item.meta || "" %></span> <span class="media-item-size"><%= item.meta %></span>
</span> </span>
</button> </button>
<% end %> <% end %>
</div> </div>
<% else %> <% else %>
<div class="sidebar-empty"> <div class="sidebar-empty">
<p><%= translated(@sidebar_data.empty_message || "No items") %></p> <p><%= translated(Map.get(@sidebar_data, :empty_message, "No items")) %></p>
</div> </div>
<% end %> <% end %>
""" """
@@ -464,12 +240,23 @@ defmodule BDS.Desktop.ShellLive do
defp render_entity_sidebar(assigns) do defp render_entity_sidebar(assigns) do
~H""" ~H"""
<%= if Enum.any?(@sidebar_data.items || []) do %> <%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %>
<div class="settings-nav-list"> <div class="settings-nav-list">
<%= for item <- @sidebar_data.items || [] do %> <%= for item <- Map.get(@sidebar_data, :items, []) do %>
<button class="chat-list-item" type="button"> <button
class={["chat-list-item", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "active")]}
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
type="button"
phx-click="open_sidebar_item"
phx-value-route={item.route}
phx-value-id={item.id}
phx-value-title={item.title}
phx-value-subtitle={translated(item.meta || "")}
>
<span class="chat-item-content"> <span class="chat-item-content">
<span class="chat-item-title"><%= item.title || "" %></span> <span class="chat-item-title"><%= item.title %></span>
<span class="chat-item-date"><%= translated(item.meta || "") %></span> <span class="chat-item-date"><%= translated(item.meta || "") %></span>
</span> </span>
</button> </button>
@@ -477,7 +264,7 @@ defmodule BDS.Desktop.ShellLive do
</div> </div>
<% else %> <% else %>
<div class="sidebar-empty"> <div class="sidebar-empty">
<p><%= translated(@sidebar_data.empty_message || "No items") %></p> <p><%= translated(Map.get(@sidebar_data, :empty_message, "No items")) %></p>
</div> </div>
<% end %> <% end %>
""" """
@@ -486,10 +273,21 @@ defmodule BDS.Desktop.ShellLive do
defp render_nav_sidebar(assigns) do defp render_nav_sidebar(assigns) do
~H""" ~H"""
<div class="settings-nav-list"> <div class="settings-nav-list">
<%= for item <- @sidebar_data.items || [] do %> <%= for item <- Map.get(@sidebar_data, :items, []) do %>
<button class="settings-nav-entry" type="button"> <button
<span class="settings-nav-entry-icon"><%= item.icon || "" %></span> class="settings-nav-entry"
<span><%= translated(item.title || "") %></span> data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
type="button"
phx-click="open_sidebar_item"
phx-value-route={item.route}
phx-value-id={item.id}
phx-value-title={translated(item.title)}
phx-value-subtitle={translated(Map.get(@sidebar_data, :subtitle, ""))}
>
<span class="settings-nav-entry-icon"><%= Map.get(item, :icon, "") %></span>
<span><%= translated(item.title) %></span>
</button> </button>
<% end %> <% end %>
</div> </div>
@@ -498,13 +296,13 @@ defmodule BDS.Desktop.ShellLive do
defp render_default_sidebar(assigns) do defp render_default_sidebar(assigns) do
~H""" ~H"""
<%= for section <- @sidebar_data.sections || [] do %> <%= for section <- Map.get(@sidebar_data, :sections, []) do %>
<section class="sidebar-section"> <section class="sidebar-section">
<div class="sidebar-section-header"> <div class="sidebar-section-header">
<span data-testid="sidebar-section-title"><%= translated(section.title) %></span> <span data-testid="sidebar-section-title"><%= translated(section.title) %></span>
</div> </div>
<div class="sidebar-section-items"> <div class="sidebar-section-items">
<%= for item <- section.items || [] do %> <%= for item <- Map.get(section, :items, []) do %>
<div class="sidebar-list-item"><%= item.title || "" %></div> <div class="sidebar-list-item"><%= item.title || "" %></div>
<% end %> <% end %>
</div> </div>
@@ -524,14 +322,14 @@ defmodule BDS.Desktop.ShellLive do
defp render_task_entries(assigns) do defp render_task_entries(assigns) do
~H""" ~H"""
<%= if Enum.empty?(@task_status.tasks || []) do %> <%= if Enum.empty?(Map.get(@task_status, :tasks, [])) do %>
<div class="panel-entry panel-empty-state"> <div class="panel-entry panel-empty-state">
<strong><%= translated("Tasks") %></strong> <strong><%= translated("Tasks") %></strong>
<span><%= translated("No background tasks running") %></span> <span><%= translated("No background tasks running") %></span>
</div> </div>
<% else %> <% else %>
<div class="task-list"> <div class="task-list">
<%= for task <- @task_status.tasks || [] do %> <%= for task <- Map.get(@task_status, :tasks, []) do %>
<div class="panel-entry task-entry"> <div class="panel-entry task-entry">
<div class="task-entry-header"> <div class="task-entry-header">
<strong><%= task.name %></strong> <strong><%= task.name %></strong>
@@ -588,7 +386,7 @@ defmodule BDS.Desktop.ShellLive do
defp activity_label(label), do: translated(label) defp activity_label(label), do: translated(label)
defp active_sidebar_label(activity_buttons, active_view, sidebar_data) do defp active_sidebar_label(activity_buttons, active_view, sidebar_data) do
Enum.find_value(activity_buttons, translated(sidebar_data.title || ""), fn button -> Enum.find_value(activity_buttons, translated(Map.get(sidebar_data, :title, "")), fn button ->
if button.id == active_view, do: activity_label(button.label), else: nil if button.id == active_view, do: activity_label(button.label), else: nil
end) end)
end end
@@ -618,6 +416,68 @@ defmodule BDS.Desktop.ShellLive do
if image_media?(item), do: "media-thumbnail has-image", else: "media-thumbnail" if image_media?(item), do: "media-thumbnail has-image", else: "media-thumbnail"
end end
defp current_tab(%{active_tab: nil}), do: nil
defp current_tab(%{tabs: tabs, active_tab: {type, id}}) do
Enum.find(tabs, &(&1.type == type and &1.id == id))
end
defp sidebar_route_atom(route) when is_atom(route), do: route
defp sidebar_route_atom(route) when is_binary(route), do: String.to_existing_atom(route)
defp tab_id_for_route(route, id) do
case Registry.editor_route(route) do
%{singleton: true} -> Atom.to_string(route)
_other -> id
end
end
defp tab_intent(route) do
case Registry.editor_route(route) do
%{singleton: true} -> :pin
_other -> :preview
end
end
defp sidebar_item_selected?(workbench, route, id) do
route_atom = sidebar_route_atom(route)
workbench.active_tab == {route_atom, tab_id_for_route(route_atom, id)}
end
defp tab_title(nil, _tab_meta), do: translated("Dashboard")
defp tab_title(tab, tab_meta) do
case Map.get(tab_meta, {tab.type, tab.id}) do
%{title: title} when is_binary(title) and title != "" -> title
_other -> default_tab_title(tab)
end
end
defp tab_subtitle(nil, _tab_meta), do: translated("dashboard.subtitle")
defp tab_subtitle(tab, tab_meta) do
case Map.get(tab_meta, {tab.type, tab.id}) do
%{subtitle: subtitle} when is_binary(subtitle) and subtitle != "" -> subtitle
_other -> "Desktop workbench content routed through the Elixir shell."
end
end
defp default_tab_title(%{type: type, id: id}) do
case Registry.editor_route(type) do
%{singleton: true} -> ShellData.route_label(type)
_other -> id
end
end
defp tab_route_label(nil), do: translated("Dashboard")
defp tab_route_label(%{type: type}), do: ShellData.route_label(type)
defp tab_icon_id(nil), do: "posts"
defp tab_icon_id(%{type: :post}), do: "posts"
defp tab_icon_id(%{type: :git_diff}), do: "git"
defp tab_icon_id(%{type: :style}), do: "settings"
defp tab_icon_id(%{type: type}), do: Atom.to_string(type)
defp media_thumbnail_glyph(mime_type) do defp media_thumbnail_glyph(mime_type) do
case String.split(to_string(mime_type || ""), "/", parts: 2) do case String.split(to_string(mime_type || ""), "/", parts: 2) do
["image", _rest] -> "IMG" ["image", _rest] -> "IMG"

View File

@@ -0,0 +1,348 @@
<div class="app" id="bds-shell-app">
<div class="window-titlebar" data-region="title-bar">
<div class="window-titlebar-menu-bar is-hidden">
<button class="window-titlebar-menu-button" type="button">File</button>
<button class="window-titlebar-menu-button" type="button">Edit</button>
<button class="window-titlebar-menu-button" type="button">View</button>
<button class="window-titlebar-menu-button" type="button">Blog</button>
<button class="window-titlebar-menu-button" type="button">Help</button>
</div>
<div class="window-titlebar-drag-region"></div>
<div class="window-titlebar-title" data-testid="window-title"><%= @page_title %></div>
<div class="window-titlebar-actions">
<button
class="window-titlebar-action-button"
data-testid="toggle-sidebar"
type="button"
phx-click="toggle_sidebar"
aria-label="Toggle sidebar"
title="Toggle sidebar"
>
<span class={["window-titlebar-sidebar-icon", if(@workbench.sidebar_visible, do: "is-active", else: "is-inactive")]}>
<span class="window-titlebar-sidebar-pane"></span>
</span>
</button>
<button
class="window-titlebar-action-button"
data-testid="toggle-panel"
type="button"
phx-click="toggle_panel"
aria-label="Toggle panel"
title="Toggle panel"
>
<span class={["window-titlebar-panel-icon", if(@workbench.panel.visible, do: "is-active", else: "is-inactive")]}>
<span class="window-titlebar-panel-pane"></span>
</span>
</button>
<button
class="window-titlebar-action-button"
data-testid="toggle-assistant"
type="button"
phx-click="toggle_assistant_sidebar"
aria-label="Toggle assistant"
title="Toggle assistant"
>
<span class={["window-titlebar-assistant-icon", if(@workbench.assistant_sidebar_visible, do: "is-active", else: "is-inactive")]}>
<span class="window-titlebar-assistant-pane"></span>
</span>
</button>
</div>
</div>
<div class="app-main">
<aside class="activity-bar" data-region="activity-bar">
<div class="activity-bar-top">
<%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :top)) do %>
<button
class={["activity-bar-item", if(button.active, do: "active")]}
data-testid="activity-button"
data-view={button.id}
data-active={to_string(button.active)}
type="button"
phx-click="select_view"
phx-value-view={button.id}
title={activity_label(button.label)}
aria-label={activity_label(button.label)}
>
<%= raw(ShellData.activity_icon(button.id)) %>
</button>
<% end %>
</div>
<div class="activity-bar-bottom">
<%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :bottom)) do %>
<button
class={["activity-bar-item", if(button.active, do: "active")]}
data-testid="activity-button"
data-view={button.id}
data-active={to_string(button.active)}
type="button"
phx-click="select_view"
phx-value-view={button.id}
title={activity_label(button.label)}
aria-label={activity_label(button.label)}
>
<%= raw(ShellData.activity_icon(button.id)) %>
</button>
<% end %>
</div>
</aside>
<section
class={["sidebar-shell", if(not @workbench.sidebar_visible, do: "is-hidden")]}
data-testid="sidebar-shell"
style={"width: #{@workbench.sidebar_width}px;"}
>
<div class="sidebar" data-region="sidebar">
<div class="sidebar-content sidebar-body">
<div class="sidebar-section">
<div class="sidebar-section-header">
<span><%= String.upcase(sidebar_header_label(@sidebar_header)) %></span>
</div>
</div>
<%= render_sidebar_body(assigns) %>
</div>
</div>
<div class="resizable-panel-divider sidebar-divider" data-resize="sidebar" data-role="resize-handle"></div>
</section>
<main class="app-content" data-region="content">
<div class="tab-bar" data-region="tab-bar">
<%= if Enum.empty?(@workbench.tabs) do %>
<div class="tab-bar-empty"><%= translated("Dashboard") %></div>
<% else %>
<div class="tab-bar-tabs">
<%= for tab <- @workbench.tabs do %>
<button
class={["tab", if(@workbench.active_tab == {tab.type, tab.id}, do: "active"), if(tab.is_transient, do: "transient")]}
data-tab-type={tab.type}
data-tab-id={tab.id}
type="button"
phx-click="select_tab"
phx-value-type={tab.type}
phx-value-id={tab.id}
>
<span class="tab-icon"><%= raw(ShellData.activity_icon(tab_icon_id(tab))) %></span>
<span class="tab-title"><%= tab_title(tab, @tab_meta) %></span>
<span class="tab-close" aria-hidden="true">×</span>
</button>
<% end %>
</div>
<% end %>
</div>
<section class="editor-shell" data-region="editor">
<%= if is_nil(@current_tab) do %>
<div class="editor-empty">
<div class="dashboard-content">
<h1 data-testid="editor-title"><%= translated("dashboard.title") %></h1>
<p class="text-muted"><%= translated("dashboard.subtitle") %></p>
<div class="dashboard-stats">
<div class="stat-card">
<div class="stat-number"><%= @dashboard.post_stats.total_posts || 0 %></div>
<div class="stat-label"><%= translated("dashboard.stats.totalPosts") %></div>
<div class="stat-breakdown">
<span class="stat-tag stat-published"><%= translated("dashboard.stats.published", %{count: @dashboard.post_stats.published_count || 0}) %></span>
<span class="stat-tag stat-draft"><%= translated("dashboard.stats.drafts", %{count: @dashboard.post_stats.draft_count || 0}) %></span>
<%= if (@dashboard.post_stats.archived_count || 0) > 0 do %>
<span class="stat-tag stat-archived"><%= translated("dashboard.stats.archived", %{count: @dashboard.post_stats.archived_count || 0}) %></span>
<% end %>
</div>
</div>
<div class="stat-card">
<div class="stat-number"><%= @dashboard.media_stats.media_count || 0 %></div>
<div class="stat-label"><%= translated("dashboard.stats.mediaFiles") %></div>
<div class="stat-breakdown">
<span class="stat-tag"><%= translated("dashboard.stats.images", %{count: @dashboard.media_stats.image_count || 0}) %></span>
<span class="stat-tag"><%= ShellData.format_bytes(@dashboard.media_stats.total_bytes || 0) %></span>
</div>
</div>
<div class="stat-card">
<div class="stat-number"><%= length(@dashboard_tag_cloud_items) %></div>
<div class="stat-label"><%= translated("dashboard.stats.tags") %></div>
<div class="stat-breakdown">
<span class="stat-tag"><%= translated("dashboard.stats.categories", %{count: length(@dashboard_category_counts)}) %></span>
</div>
</div>
</div>
<%= if Enum.any?(@dashboard_timeline_entries) do %>
<div class="dashboard-section">
<h4><%= translated("dashboard.section.postsOverTime") %></h4>
<div class="timeline-chart">
<%= for entry <- @dashboard_timeline_entries do %>
<div class="timeline-bar-container">
<div class="timeline-bar" style={"height: #{timeline_height(entry, @dashboard_timeline_entries)}%"}>
<span class="timeline-bar-count"><%= entry.count || 0 %></span>
</div>
<div class="timeline-bar-label">
<span class="timeline-bar-label-month"><%= ShellData.format_dashboard_month(entry.year, entry.month) %></span>
<span class="timeline-bar-label-year"><%= entry.year %></span>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<%= if Enum.any?(@dashboard_tag_cloud_items) do %>
<div class="dashboard-section">
<h4><%= translated("dashboard.section.tags") %></h4>
<div class="tag-cloud">
<%= for item <- @dashboard_tag_cloud_items do %>
<span class={["dashboard-tag", if(item.color, do: "has-color")]} style={ShellData.render_dashboard_tag_style(item)} title={ShellData.dashboard_post_count_label(item.count)}><%= item.tag %></span>
<% end %>
</div>
</div>
<% end %>
<%= if Enum.any?(@dashboard_category_counts) do %>
<div class="dashboard-section">
<h4><%= translated("dashboard.section.categories") %></h4>
<div class="tag-cloud">
<%= for category <- @dashboard_category_counts do %>
<span class="dashboard-tag dashboard-category" title={ShellData.dashboard_post_count_label(category.count || 0)}>
<%= category.category || "" %>
<span class="tag-count"><%= category.count || 0 %></span>
</span>
<% end %>
</div>
</div>
<% end %>
<%= if Enum.any?(@dashboard_recent_posts) do %>
<div class="dashboard-section">
<h4><%= translated("dashboard.section.recentlyUpdated") %></h4>
<div class="recent-posts-list">
<%= for post <- @dashboard_recent_posts do %>
<button class="recent-post-item" type="button">
<span class="recent-post-title"><%= post.title || "" %></span>
<span class={"recent-post-status status-#{post.status || "draft"}"}><%= ShellData.dashboard_status_label(post.status || "draft") %></span>
<span class="recent-post-date"><%= ShellData.format_dashboard_date(post.updated_at) %></span>
</button>
<% end %>
</div>
</div>
<% end %>
<div class="dashboard-inspector-meta" hidden>
<%= for item <- @editor_meta do %>
<section class="editor-meta-row">
<strong data-testid="editor-meta-label"><%= translated(item.label) %></strong>
<span><%= translated(item.value) %></span>
</section>
<% end %>
</div>
</div>
</div>
<% else %>
<div class="editor-frame">
<section class="editor-main">
<div class="editor-kicker"><%= tab_route_label(@current_tab) %></div>
<h1 class="editor-title" data-testid="editor-title"><%= tab_title(@current_tab, @tab_meta) %></h1>
<p class="editor-subtitle"><%= tab_subtitle(@current_tab, @tab_meta) %></p>
<div class="editor-toolbar">
<button class="editor-toolbar-button" type="button">Open</button>
<button class="editor-toolbar-button" type="button">Preview</button>
<button class="editor-toolbar-button" type="button">Metadata</button>
</div>
<div class="editor-section">
<h2><%= tab_title(@current_tab, @tab_meta) %></h2>
<p>Desktop workbench content routed through the Elixir shell.</p>
</div>
</section>
<aside class="editor-meta">
<%= for item <- @editor_meta do %>
<section class="editor-meta-row">
<strong data-testid="editor-meta-label"><%= translated(item.label) %></strong>
<span><%= translated(item.value) %></span>
</section>
<% end %>
</aside>
</div>
<% end %>
</section>
<section class={["panel-shell", if(not @workbench.panel.visible, do: "is-hidden")]} data-region="panel">
<div class="panel-header">
<div class="panel-tabs">
<%= for tab <- @panel_tabs do %>
<button
class={["panel-tab", if(@workbench.panel.active_tab == tab, do: "active")]}
type="button"
phx-click="select_panel_tab"
phx-value-tab={tab}
>
<%= panel_tab_label(tab) %>
</button>
<% end %>
</div>
</div>
<div class="panel-content">
<%= render_panel_body(assigns) %>
</div>
</section>
</main>
<section
class={["assistant-sidebar-shell", if(not @workbench.assistant_sidebar_visible, do: "is-hidden")]}
data-testid="assistant-shell"
style={"width: #{@workbench.assistant_sidebar_width}px;"}
>
<div class="resizable-panel-divider assistant-divider" data-resize="assistant" data-role="resize-handle"></div>
<aside class="assistant-sidebar" data-region="assistant-sidebar">
<div class="assistant-header">
<strong><%= translated("Assistant") %></strong>
</div>
<div class="assistant-content">
<%= for card <- @assistant_cards do %>
<section class="assistant-card">
<strong><%= translated(card.label) %></strong>
<span><%= translated(card.text) %></span>
</section>
<% end %>
</div>
</aside>
</section>
</div>
<footer class="status-bar" data-region="status-bar" data-testid="status-bar">
<div class="status-bar-left">
<div class="project-selector">
<button class="project-selector-trigger" type="button" title={translated("Switch project")}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="project-icon">
<path d="M14.5 3H7.71l-.85-.85A.5.5 0 0 0 6.5 2h-5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-10a.5.5 0 0 0-.5-.5zm-13 1h5.29l.85.85c.1.1.23.15.36.15h6.5v9h-13V4z"></path>
</svg>
<span class="project-name"><%= if @current_project, do: @current_project.name, else: "My Blog" %></span>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" class="dropdown-arrow">
<path d="M4.5 5.5L8 9l3.5-3.5h-7z"></path>
</svg>
</button>
</div>
<button class="status-bar-item status-bar-task-button" data-testid="status-task-button" type="button" phx-click="toggle_panel">
<span><%= @status.left.running_task_message || translated("Idle") %></span>
<%= if (@status.left.running_task_overflow || 0) > 0 do %>
<span class="status-bar-count">+<%= @status.left.running_task_overflow %></span>
<% end %>
</button>
</div>
<div class="status-bar-right">
<span class="status-bar-item"><%= @status.right.post_count %></span>
<span class="status-bar-item"><%= @status.right.media_count %></span>
<span class="status-bar-item theme-badge"><%= @status.right.theme_badge %></span>
<button class={["status-bar-item", "offline-badge", if(@status.right.offline_mode, do: "active")]} data-testid="status-offline-button" type="button" phx-click="toggle_offline_mode" title={translated("Toggle offline mode")}>✈</button>
<label class="status-bar-item language-badge">
<span><%= translated("UI") %></span>
<select class="status-bar-language-select">
<%= for language <- @supported_ui_languages do %>
<option selected={language.code == @page_language} value={language.code}><%= language.flag %></option>
<% end %>
</select>
</label>
<span class="status-bar-item brand"><%= @status.right.brand %></span>
</div>
</footer>
</div>

View File

@@ -192,7 +192,7 @@ defmodule BDS.Sidecar do
else else
inner inner
|> String.split(",", trim: true) |> String.split(",", trim: true)
|> Enum.map(&(String.trim(&1) |> parse_scalar(nil))) |> Enum.map(&parse_scalar(nil, String.trim(&1)))
end end
end end

View File

@@ -62,6 +62,13 @@ body {
font-size: var(--vscode-font-size); font-size: var(--vscode-font-size);
} }
body > [data-phx-session],
body > [data-phx-main] {
width: 100%;
height: 100%;
min-height: 0;
}
button { button {
font: inherit; font: inherit;
} }

View File

@@ -10,6 +10,8 @@ defmodule BDS.Desktop.ShellLiveTest do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) {:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-testid="sidebar-shell") assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(data-testid="status-bar")
assert html =~ ~s(data-testid="status-task-button")
assert html =~ ~s(class="panel-shell is-hidden") assert html =~ ~s(class="panel-shell is-hidden")
assert html =~ ~s(data-testid="activity-button") assert html =~ ~s(data-testid="activity-button")
assert html =~ ~s(data-view="posts") assert html =~ ~s(data-view="posts")
@@ -38,5 +40,20 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(aria-label="Media") assert html =~ ~s(aria-label="Media")
assert html =~ ~s(data-view="media") assert html =~ ~s(data-view="media")
html =
view
|> element("[data-testid='activity-button'][data-view='settings']")
|> render_click()
assert html =~ ~s(data-testid="sidebar-open-item")
html =
view
|> element("[data-testid='sidebar-open-item'][data-item-id='settings-project']")
|> render_click()
assert html =~ ~s(data-tab-type="settings")
assert html =~ ">Settings<"
end end
end end