feat(workbench): add merged main menu style (#561)

* feat(workbench): add merged main menu style

* fix(workbench): restore focus on a second Alt and keep closed menus closed
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-31 20:47:32 +08:00
1 джерело bdadecdf91
коміт e7740e4032
10 files changed
+562 -59

No files matched your search

+46
Переглянути файл
@@ -0,0 +1,46 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { usePlatform } from "@/platform/platformInfo";
import { validateAppearanceExtras } from "../model/behaviorSchema";
import { useSettingsDialog } from "../model/staging";
import { BEHAVIOR_PAGES } from "../pages/behavior";
const savedPlatform = usePlatform.getState();
const savedDialog = useSettingsDialog.getState();
afterEach(() => {
usePlatform.setState(savedPlatform);
useSettingsDialog.setState(savedDialog);
});
const option = BEHAVIOR_PAGES.find((p) => p.id === "appearance")!
.sections!.find((s) => s.title === "UI Options")!
.options.find((o) => o.kind === "custom")!;
if (option.kind !== "custom") throw new Error("UI Options missing");
const MainMenuStyleRow = option.component;
describe("Main menu appearance setting", () => {
it.each(["windows", "linux"])("offers both styles on a frameless %s title bar", async (os) => {
usePlatform.setState({ os, framelessTitleBar: true });
render(<MainMenuStyleRow />);
const select = screen.getByRole("button", { name: "Main menu" });
expect(select).toHaveTextContent("Hamburger button");
fireEvent.click(select);
fireEvent.click(await screen.findByRole("option", { name: "Merged with main toolbar" }));
expect(select).toHaveTextContent("Merged with main toolbar");
expect(useSettingsDialog.getState().staged["global|appearance.mainMenuStyle"]?.value).toBe("merged");
});
it.each(["macos", "web", "linux"])("hides the setting without the frameless menu (%s)", (os) => {
usePlatform.setState({ os, framelessTitleBar: false });
render(<MainMenuStyleRow />);
expect(screen.queryByText("Main menu")).toBeNull();
expect(screen.queryByRole("button", { name: "Main menu" })).toBeNull();
act(() => usePlatform.setState({ os: "windows", framelessTitleBar: true }));
expect(screen.getByRole("button", { name: "Main menu" })).toBeInTheDocument();
});
it("preserves the hamburger default for existing or invalid settings", () => {
expect(validateAppearanceExtras({}).mainMenuStyle).toBe("hamburger");
expect(validateAppearanceExtras({ mainMenuStyle: "unknown" }).mainMenuStyle).toBe("hamburger");
expect(validateAppearanceExtras({ mainMenuStyle: "merged" }).mainMenuStyle).toBe("merged");
});
});
+3
Переглянути файл
@@ -27,6 +27,7 @@ export interface AppearanceExtras {
projectColorsInToolbar: boolean;
dndWithAltOnly: boolean;
menuIcons: boolean;
mainMenuStyle: "hamburger" | "merged";
ideAntialiasing: Antialiasing;
editorAntialiasing: Antialiasing;
showToolWindowBars: boolean;
@@ -78,6 +79,7 @@ export const DEFAULT_APPEARANCE_EXTRAS: AppearanceExtras = {
projectColorsInToolbar: true,
dndWithAltOnly: false,
menuIcons: true,
mainMenuStyle: "hamburger",
ideAntialiasing: "greyscale",
editorAntialiasing: "greyscale",
showToolWindowBars: true,
@@ -127,6 +129,7 @@ export function validateAppearanceExtras(raw: unknown): AppearanceExtras {
projectColorsInToolbar: b("projectColorsInToolbar"),
dndWithAltOnly: b("dndWithAltOnly"),
menuIcons: b("menuIcons"),
mainMenuStyle: oneOf(a.mainMenuStyle, ["hamburger", "merged"], d.mainMenuStyle),
ideAntialiasing: oneOf(a.ideAntialiasing, AA, d.ideAntialiasing),
editorAntialiasing: oneOf(a.editorAntialiasing, AA, d.editorAntialiasing),
showToolWindowBars: b("showToolWindowBars"),
+26
Переглянути файл
@@ -9,6 +9,7 @@ import { FontPicker } from "../components/FontPicker";
import { useId, useRef, useState, type ReactNode } from "react";
import clsx from "clsx";
import { shortcutLabel } from "@/platform/keymap";
import { usePlatform } from "@/platform/platformInfo";
import { isTauri } from "@/platform/ipc";
import { SCHEMES as EDITOR_SCHEMES, listSchemes, customSchemes } from "@/platform/colorScheme";
import { formatTimestamp } from "@/platform/dateFormat";
@@ -223,6 +224,25 @@ function Grid({ left, right }: { left: ReactNode; right: ReactNode }) {
);
}
function MainMenuStyleRow() {
const frameless = usePlatform((s) => s.framelessTitleBar);
const [mainMenuStyle, setMainMenuStyle] = useSetting<"hamburger" | "merged">("appearance.mainMenuStyle");
if (!frameless) return null;
return (
<Row label="Main menu">
<Select
value={mainMenuStyle ?? "hamburger"}
options={[
{ value: "hamburger", label: "Hamburger button" },
{ value: "merged", label: "Merged with main toolbar" },
]}
onChange={setMainMenuStyle}
aria-label="Main menu"
/>
</Row>
);
}
function UiOptionsGrid() {
return (
<Grid
@@ -521,6 +541,12 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
{
title: "UI Options",
options: [
{
kind: "custom",
label: "Main menu",
keywords: ["Hamburger button", "Merged with main toolbar"],
component: MainMenuStyleRow,
},
{
kind: "custom",
label: "Compact mode",