fix: more alignment

This commit is contained in:
2026-04-26 09:13:50 +02:00
parent a1456592bd
commit b377951054
7 changed files with 261 additions and 10 deletions

View File

@@ -2,6 +2,8 @@
"chat.tools.terminal.autoApprove": { "chat.tools.terminal.autoApprove": {
"mix": true, "mix": true,
"allium": true, "allium": true,
"command": true "command": true,
"printf": true,
"git ls-tree": true
} }
} }

View File

@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive do
import Phoenix.HTML import Phoenix.HTML
alias BDS.Desktop.{FolderPicker, ShellCommands, ShellData} alias BDS.Desktop.{FolderPicker, ShellCommands, ShellData}
alias BDS.Desktop.MenuBar, as: DesktopMenuBar
alias BDS.Git alias BDS.Git
alias BDS.Media.Media alias BDS.Media.Media
alias BDS.PostLinks alias BDS.PostLinks
@@ -46,6 +47,9 @@ defmodule BDS.Desktop.ShellLive do
|> assign(:page_language, ShellData.ui_language()) |> assign(:page_language, ShellData.ui_language())
|> assign(:client_shortcuts, Commands.client_shortcuts()) |> assign(:client_shortcuts, Commands.client_shortcuts())
|> assign(:offline_mode, true) |> assign(:offline_mode, true)
|> assign(:is_mac_ui, mac_ui?())
|> assign(:menu_groups, titlebar_menu_groups())
|> assign(:titlebar_menu_group, nil)
|> assign(:tab_meta, %{}) |> assign(:tab_meta, %{})
|> assign(:project_menu_open, false) |> assign(:project_menu_open, false)
|> assign(:sidebar_filters_by_view, %{}) |> assign(:sidebar_filters_by_view, %{})
@@ -337,6 +341,33 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, handle_native_menu_action(socket, action)} {:noreply, handle_native_menu_action(socket, action)}
end end
def handle_event("toggle_titlebar_menu", %{"group" => group}, socket) do
next_group = if socket.assigns.titlebar_menu_group == group, do: nil, else: group
{:noreply, assign(socket, :titlebar_menu_group, next_group)}
end
def handle_event("hover_titlebar_menu", %{"group" => group}, socket) do
socket =
if socket.assigns.titlebar_menu_group do
assign(socket, :titlebar_menu_group, group)
else
socket
end
{:noreply, socket}
end
def handle_event("close_titlebar_menu", _params, socket) do
{:noreply, assign(socket, :titlebar_menu_group, nil)}
end
def handle_event("titlebar_menu_action", %{"action" => action}, socket) do
{:noreply,
socket
|> assign(:titlebar_menu_group, nil)
|> handle_native_menu_action(action)}
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()
@@ -395,6 +426,7 @@ defmodule BDS.Desktop.ShellLive do
|> 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(:menu_groups, socket.assigns[:menu_groups] || titlebar_menu_groups())
|> assign(:current_tab, current_tab(workbench)) |> assign(:current_tab, current_tab(workbench))
end end
@@ -1349,6 +1381,21 @@ defmodule BDS.Desktop.ShellLive do
|> Enum.map_join(" ", &String.capitalize/1) |> Enum.map_join(" ", &String.capitalize/1)
end end
defp titlebar_menu_groups do
DesktopMenuBar.groups(dev_mode?: Application.get_env(:bds, :dev_routes, false))
end
defp active_titlebar_menu_group(assigns) do
Enum.find(assigns.menu_groups || [], fn group -> Atom.to_string(group.id) == assigns.titlebar_menu_group end)
end
defp mac_ui? do
case Application.get_env(:bds, :shell_platform) do
nil -> match?({:unix, :darwin}, :os.type())
platform -> match?({:unix, :darwin}, platform)
end
end
defp post_link_entries(assigns) do defp post_link_entries(assigns) do
case assigns.current_tab do case assigns.current_tab do
%{type: :post, id: post_id} -> %{type: :post, id: post_id} ->

View File

@@ -1,12 +1,29 @@
<div class="app" id="bds-shell-app" phx-hook="AppShell" data-shortcuts={encoded_shortcuts(@client_shortcuts)}> <div class="app" id="bds-shell-app" phx-hook="AppShell" data-shortcuts={encoded_shortcuts(@client_shortcuts)}>
<div class="window-titlebar" data-region="title-bar"> <div
<div class="window-titlebar-menu-bar is-hidden"> class={["window-titlebar", if(@is_mac_ui, do: "is-mac")]}
<button class="window-titlebar-menu-button" type="button">File</button> data-region="title-bar"
<button class="window-titlebar-menu-button" type="button">Edit</button> data-testid="window-titlebar"
<button class="window-titlebar-menu-button" type="button">View</button> data-open-menu-group={@titlebar_menu_group || ""}
<button class="window-titlebar-menu-button" type="button">Blog</button> >
<button class="window-titlebar-menu-button" type="button">Help</button> <%= unless @is_mac_ui do %>
</div> <div class="window-titlebar-menu-bar" data-testid="window-titlebar-menu-bar">
<%= for group <- @menu_groups do %>
<button
class={[
"window-titlebar-menu-button",
if(@titlebar_menu_group == Atom.to_string(group.id), do: "is-active")
]}
data-testid="window-titlebar-menu-button"
data-menu-group={group.id}
type="button"
phx-click="toggle_titlebar_menu"
phx-mouseenter="hover_titlebar_menu"
phx-value-group={group.id}
aria-label={group.label}
><%= group.label %></button>
<% end %>
</div>
<% end %>
<div class="window-titlebar-drag-region"></div> <div class="window-titlebar-drag-region"></div>
<div class="window-titlebar-title" data-testid="window-title"><%= @page_title %></div> <div class="window-titlebar-title" data-testid="window-title"><%= @page_title %></div>
<div class="window-titlebar-actions"> <div class="window-titlebar-actions">
@@ -47,6 +64,36 @@
</span> </span>
</button> </button>
</div> </div>
<%= if not @is_mac_ui do %>
<%= if group = active_titlebar_menu_group(assigns) do %>
<div
class="window-titlebar-menu-dropdown"
data-testid="window-titlebar-menu-dropdown"
phx-click-away="close_titlebar_menu"
>
<%= for item <- group.items do %>
<%= if item.separator do %>
<div class="window-titlebar-menu-separator"></div>
<% else %>
<button
class="window-titlebar-menu-item"
data-testid="window-titlebar-menu-item"
data-menu-action={item.id}
type="button"
phx-click="titlebar_menu_action"
phx-value-action={item.id}
aria-label={item.label}
>
<span class="window-titlebar-menu-item-label"><%= item.label %></span>
<%= if item.shortcut do %>
<span class="window-titlebar-menu-item-accelerator"><%= item.shortcut %></span>
<% end %>
</button>
<% end %>
<% end %>
</div>
<% end %>
<% end %>
</div> </div>
<div class="app-main"> <div class="app-main">

View File

@@ -146,6 +146,10 @@ button {
background-color: var(--vscode-toolbar-hoverBackground); background-color: var(--vscode-toolbar-hoverBackground);
} }
.window-titlebar-menu-button.is-active {
background-color: var(--vscode-toolbar-hoverBackground);
}
.window-titlebar-menu-button:focus, .window-titlebar-menu-button:focus,
.window-titlebar-menu-button:focus-visible, .window-titlebar-menu-button:focus-visible,
.window-titlebar-action-button:focus, .window-titlebar-action-button:focus,
@@ -154,6 +158,61 @@ button {
box-shadow: none; box-shadow: none;
} }
.window-titlebar-menu-dropdown {
position: absolute;
top: 30px;
left: 6px;
min-width: 210px;
padding: 6px;
display: flex;
flex-direction: column;
gap: 2px;
background-color: var(--vscode-menu-background, var(--vscode-editorWidget-background));
border: 1px solid var(--vscode-menu-border, var(--vscode-panel-border));
border-radius: 6px;
box-shadow: var(--vscode-widget-shadow, 0 8px 24px rgba(0, 0, 0, 0.4));
app-region: no-drag;
-webkit-app-region: no-drag;
z-index: 10;
}
.window-titlebar-menu-item {
border: none;
background: transparent;
color: var(--vscode-menu-foreground, var(--vscode-foreground));
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
text-align: left;
border-radius: 4px;
padding: 6px 8px;
font-size: 12px;
cursor: pointer;
}
.window-titlebar-menu-item:focus,
.window-titlebar-menu-item:focus-visible {
outline: none;
box-shadow: none;
background-color: var(--vscode-toolbar-hoverBackground);
}
.window-titlebar-menu-item:hover,
.window-titlebar-menu-item.is-keyboard-active {
background-color: var(--vscode-menu-selectionBackground, var(--vscode-toolbar-hoverBackground));
}
.window-titlebar-menu-item-accelerator {
opacity: 0.8;
}
.window-titlebar-menu-separator {
height: 1px;
margin: 4px 2px;
background-color: var(--vscode-menu-separatorBackground, rgba(255, 255, 255, 0.08));
}
.window-titlebar-drag-region { .window-titlebar-drag-region {
flex: 1; flex: 1;
height: 100%; height: 100%;

View File

@@ -183,6 +183,27 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}; };
this.menuIsOpen = () => {
const titlebar = this.el.querySelector("[data-testid='window-titlebar']");
return Boolean(titlebar?.dataset.openMenuGroup);
};
this.handleTitlebarPointerDown = (event) => {
if (!this.menuIsOpen()) {
return;
}
if (event.target.closest("[data-testid='window-titlebar-menu-button']")) {
return;
}
if (event.target.closest("[data-testid='window-titlebar-menu-dropdown']")) {
return;
}
this.pushEvent("close_titlebar_menu", {});
};
this.handleChange = (event) => { this.handleChange = (event) => {
const select = event.target.closest(".status-bar-language-select"); const select = event.target.closest(".status-bar-language-select");
@@ -216,8 +237,16 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
}; };
this.handleEscapeKey = (event) => {
if (event.key === "Escape" && this.menuIsOpen()) {
this.pushEvent("close_titlebar_menu", {});
}
};
window.addEventListener("bds:native-menu-action", this.handleNativeMenuAction); window.addEventListener("bds:native-menu-action", this.handleNativeMenuAction);
window.addEventListener("keydown", this.handleShortcutKeyDown, true); window.addEventListener("keydown", this.handleShortcutKeyDown, true);
window.addEventListener("keydown", this.handleEscapeKey, true);
window.addEventListener("pointerdown", this.handleTitlebarPointerDown, true);
this.el.addEventListener("change", this.handleChange); this.el.addEventListener("change", this.handleChange);
}, },
@@ -226,6 +255,8 @@ document.addEventListener("DOMContentLoaded", () => {
this.el.removeEventListener("change", this.handleChange); this.el.removeEventListener("change", this.handleChange);
window.removeEventListener("bds:native-menu-action", this.handleNativeMenuAction); window.removeEventListener("bds:native-menu-action", this.handleNativeMenuAction);
window.removeEventListener("keydown", this.handleShortcutKeyDown, true); window.removeEventListener("keydown", this.handleShortcutKeyDown, true);
window.removeEventListener("keydown", this.handleEscapeKey, true);
window.removeEventListener("pointerdown", this.handleTitlebarPointerDown, true);
if (this.destroyOverlaySync) { if (this.destroyOverlaySync) {
this.destroyOverlaySync(); this.destroyOverlaySync();
} }

View File

@@ -25,6 +25,16 @@ defmodule BDS.Desktop.ShellLiveTest do
{:ok, project} = Projects.create_project(%{name: "Shell Project", data_path: temp_dir}) {:ok, project} = Projects.create_project(%{name: "Shell Project", data_path: temp_dir})
{:ok, _project} = Projects.set_active_project(project.id) {:ok, _project} = Projects.set_active_project(project.id)
original_shell_platform = Application.get_env(:bds, :shell_platform)
on_exit(fn ->
if is_nil(original_shell_platform) do
Application.delete_env(:bds, :shell_platform)
else
Application.put_env(:bds, :shell_platform, original_shell_platform)
end
end)
%{project: project, temp_dir: temp_dir} %{project: project, temp_dir: temp_dir}
end end
@@ -95,6 +105,59 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(class="tab-bar-empty") assert html =~ ~s(class="tab-bar-empty")
end end
test "titlebar menu stays hidden on macos because the native menu owns it" do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(class="window-titlebar is-mac")
refute html =~ ~s(data-testid="window-titlebar-menu-bar")
refute html =~ ~s(data-testid="window-titlebar-menu-button")
refute html =~ ~s(data-testid="window-titlebar-menu-dropdown")
html =
render_hook(view, "native_menu_action", %{"action" => "edit_preferences"})
assert html =~ ~s(data-tab-type="settings")
assert html =~ ">Settings<"
end
test "titlebar menu matches the old shell contract on windows and linux" do
Application.put_env(:bds, :shell_platform, {:unix, :linux})
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
refute html =~ ~s(class="window-titlebar is-mac")
assert html =~ ~s(data-testid="window-titlebar-menu-bar")
assert html =~ ~s(data-testid="window-titlebar-menu-button")
assert html =~ ~s(data-menu-group="file")
assert html =~ ~s(>File<)
html =
view
|> element("[data-testid='window-titlebar-menu-button'][data-menu-group='file']")
|> render_click()
assert html =~ ~s(data-testid="window-titlebar-menu-dropdown")
assert html =~ ~s(data-testid="window-titlebar-menu-item")
assert html =~ ~s(data-menu-action="new_post")
assert html =~ ~s(>New Post<)
html =
view
|> element("[data-testid='window-titlebar-menu-button'][data-menu-group='edit']")
|> render_click()
assert html =~ ~s(data-menu-action="edit_preferences")
html =
view
|> element("[data-testid='window-titlebar-menu-item'][data-menu-action='edit_preferences']")
|> render_click()
assert html =~ ~s(data-tab-type="settings")
assert html =~ ">Settings<"
refute html =~ ~s(data-testid="window-titlebar-menu-dropdown")
end
test "sidebar open supports preview and pin intents for entity tabs" do test "sidebar open supports preview and pin intents for entity tabs" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)

View File

@@ -101,7 +101,9 @@ defmodule BDS.DesktopTest do
assert conn.status == 200 assert conn.status == 200
assert conn.resp_body =~ ~s(class="app") assert conn.resp_body =~ ~s(class="app")
assert conn.resp_body =~ ~s(class="window-titlebar") assert conn.resp_body =~ ~s(data-testid="window-titlebar")
assert conn.resp_body =~ ~s(class="window-titlebar is-mac")
refute conn.resp_body =~ ~s(data-testid="window-titlebar-menu-bar")
assert conn.resp_body =~ ~s(class="activity-bar") assert conn.resp_body =~ ~s(class="activity-bar")
assert conn.resp_body =~ ~s(class="sidebar") assert conn.resp_body =~ ~s(class="sidebar")
assert conn.resp_body =~ ~s(class="status-bar") assert conn.resp_body =~ ~s(class="status-bar")