Keep workspace focus mode scoped and easy to exit (#2151)
* fix(workspace): keep focus mode scoped and easy to exit Route focus mode through the active workspace so persisted state cannot hide chrome on settings or other screens. Add a visible exit control and keep desktop window chrome aligned and visually quiet. * fix(app): clarify muted chrome semantics
This commit is contained in:
@@ -40,6 +40,8 @@ The rule, condensed: text that _names_ a surface or a group is `medium`. Text th
|
|||||||
|
|
||||||
Foreground is for the thing being acted on: row titles, section headings, the selected sidebar item. `foregroundMuted` is for context: hints, descriptions, secondary metadata, idle sidebar items, placeholders, status text.
|
Foreground is for the thing being acted on: row titles, section headings, the selected sidebar item. `foregroundMuted` is for context: hints, descriptions, secondary metadata, idle sidebar items, placeholders, status text.
|
||||||
|
|
||||||
|
`foregroundExtraMuted` is reserved for passive chrome that must sit behind muted text, such as an always-visible window control. Use the solid token instead of lowering SVG opacity; per-path opacity makes overlapping icon strokes render unevenly. Interactive hover and pressed states return to `foreground`.
|
||||||
|
|
||||||
Accent is the one CTA per surface. A `<Button variant="default">` filled with `accent` appears at most once on a page. Most pages have zero — settings is mostly toggles and text, the workspace pane is mostly content, the chat composer is the input itself.
|
Accent is the one CTA per surface. A `<Button variant="default">` filled with `accent` appears at most once on a page. Most pages have zero — settings is mostly toggles and text, the workspace pane is mostly content, the chat composer is the input itself.
|
||||||
|
|
||||||
Destructive is a color, not a click. Restart-daemon and remove-host are `<Button variant="outline">` in the row trailing slot; the destructive surface only appears inside the `confirmDialog` (`packages/app/src/screens/settings/host-page.tsx:541-547`). Workspace archive opens a confirm dialog before any red appears (`packages/app/src/components/sidebar-workspace-list.tsx`). Red appears after the user has indicated intent.
|
Destructive is a color, not a click. Restart-daemon and remove-host are `<Button variant="outline">` in the row trailing slot; the destructive surface only appears inside the `confirmDialog` (`packages/app/src/screens/settings/host-page.tsx:541-547`). Workspace archive opens a confirm dialog before any red appears (`packages/app/src/components/sidebar-workspace-list.tsx`). Red appears after the user has indicated intent.
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { expect, test } from "./fixtures";
|
||||||
|
|
||||||
|
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
||||||
|
|
||||||
|
async function pressFocusModeShortcut(page: import("@playwright/test").Page) {
|
||||||
|
await page.keyboard.press(`${modifier}+Shift+F`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pressSettingsShortcut(page: import("@playwright/test").Page) {
|
||||||
|
await page.keyboard.press(`${modifier}+Comma`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function blurActiveElement(page: import("@playwright/test").Page) {
|
||||||
|
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
|
||||||
|
}
|
||||||
|
|
||||||
|
test("focus mode only applies to the active workspace screen", async ({ page, withWorkspace }) => {
|
||||||
|
const workspace = await withWorkspace({ prefix: "focus-mode-boundary-" });
|
||||||
|
await workspace.navigateTo();
|
||||||
|
const exitFocusMode = page.getByRole("button", { name: "Exit focus mode" });
|
||||||
|
const settingsButton = page.getByRole("button", { name: "Settings", exact: true });
|
||||||
|
const settingsSidebar = page.getByRole("navigation", { name: "Settings" });
|
||||||
|
|
||||||
|
await expect(settingsButton).toBeVisible();
|
||||||
|
await expect(exitFocusMode).toHaveCount(0);
|
||||||
|
|
||||||
|
await blurActiveElement(page);
|
||||||
|
await pressFocusModeShortcut(page);
|
||||||
|
|
||||||
|
await expect(exitFocusMode).toBeVisible();
|
||||||
|
await expect(settingsButton).toHaveCount(0);
|
||||||
|
const workspaceUrl = page.url();
|
||||||
|
|
||||||
|
await pressSettingsShortcut(page);
|
||||||
|
|
||||||
|
await expect(settingsSidebar).toBeVisible();
|
||||||
|
await expect(exitFocusMode).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await expect(settingsSidebar).toBeVisible();
|
||||||
|
await expect(exitFocusMode).toHaveCount(0);
|
||||||
|
|
||||||
|
await pressFocusModeShortcut(page);
|
||||||
|
await page.goto(workspaceUrl);
|
||||||
|
|
||||||
|
await expect(exitFocusMode).toBeVisible();
|
||||||
|
await exitFocusMode.click();
|
||||||
|
|
||||||
|
await expect(exitFocusMode).toHaveCount(0);
|
||||||
|
await expect(settingsButton).toBeVisible();
|
||||||
|
});
|
||||||
@@ -40,6 +40,7 @@ import { FloatingPanelPortalHost } from "@/components/ui/floating-panel-portal";
|
|||||||
import { HostChooserModal, useHostChooser } from "@/hosts/host-chooser";
|
import { HostChooserModal, useHostChooser } from "@/hosts/host-chooser";
|
||||||
import {
|
import {
|
||||||
getIsElectronRuntime,
|
getIsElectronRuntime,
|
||||||
|
getIsElectronRuntimeMac,
|
||||||
HEADER_INNER_HEIGHT,
|
HEADER_INNER_HEIGHT,
|
||||||
useIsCompactFormFactor,
|
useIsCompactFormFactor,
|
||||||
} from "@/constants/layout";
|
} from "@/constants/layout";
|
||||||
@@ -108,7 +109,11 @@ import {
|
|||||||
WindowChromeRegion,
|
WindowChromeRegion,
|
||||||
WindowChromeSafeArea,
|
WindowChromeSafeArea,
|
||||||
} from "@/utils/desktop-window";
|
} from "@/utils/desktop-window";
|
||||||
import { buildOpenProjectRoute, parseServerIdFromPathname } from "@/utils/host-routes";
|
import {
|
||||||
|
buildOpenProjectRoute,
|
||||||
|
parseHostWorkspaceRouteFromPathname,
|
||||||
|
parseServerIdFromPathname,
|
||||||
|
} from "@/utils/host-routes";
|
||||||
import { buildNotificationRoute, resolveNotificationTarget } from "@/utils/notification-routing";
|
import { buildNotificationRoute, resolveNotificationTarget } from "@/utils/notification-routing";
|
||||||
import { navigateToAgent } from "@/utils/navigate-to-agent";
|
import { navigateToAgent } from "@/utils/navigate-to-agent";
|
||||||
import {
|
import {
|
||||||
@@ -425,7 +430,6 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
|
|||||||
const openDesktopAgentList = usePanelStore((state) => state.openDesktopAgentList);
|
const openDesktopAgentList = usePanelStore((state) => state.openDesktopAgentList);
|
||||||
const closeDesktopAgentList = usePanelStore((state) => state.closeDesktopAgentList);
|
const closeDesktopAgentList = usePanelStore((state) => state.closeDesktopAgentList);
|
||||||
const closeDesktopFileExplorer = usePanelStore((state) => state.closeDesktopFileExplorer);
|
const closeDesktopFileExplorer = usePanelStore((state) => state.closeDesktopFileExplorer);
|
||||||
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
|
|
||||||
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
||||||
const isDesktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
const isDesktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
||||||
const isDesktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
const isDesktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||||
@@ -442,6 +446,8 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
|
|||||||
const isCompactLayout = useIsCompactFormFactor();
|
const isCompactLayout = useIsCompactFormFactor();
|
||||||
useCompactWebViewportZoomLock(isCompactLayout);
|
useCompactWebViewportZoomLock(isCompactLayout);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const isWorkspaceRoute = parseHostWorkspaceRouteFromPathname(pathname) !== null;
|
||||||
|
const isWorkspaceFocusModeEnabled = isWorkspaceRoute && isFocusModeEnabled;
|
||||||
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
|
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
|
||||||
const toggleAgentList = isCompactLayout ? toggleMobileAgentList : toggleDesktopAgentList;
|
const toggleAgentList = isCompactLayout ? toggleMobileAgentList : toggleDesktopAgentList;
|
||||||
const toggleDesktopSidebars = useCallback(() => {
|
const toggleDesktopSidebars = useCallback(() => {
|
||||||
@@ -470,7 +476,6 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
|
|||||||
isMobile: isCompactLayout,
|
isMobile: isCompactLayout,
|
||||||
toggleAgentList,
|
toggleAgentList,
|
||||||
toggleBothSidebars: toggleDesktopSidebars,
|
toggleBothSidebars: toggleDesktopSidebars,
|
||||||
toggleFocusMode,
|
|
||||||
cycleTheme,
|
cycleTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -479,11 +484,11 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
|
|||||||
|
|
||||||
const appContentMinimumWidth = resolveDesktopAppContentMinimum({
|
const appContentMinimumWidth = resolveDesktopAppContentMinimum({
|
||||||
isSettingsRoute: pathname.includes("/settings"),
|
isSettingsRoute: pathname.includes("/settings"),
|
||||||
isWorkspaceExplorerOpen: pathname.includes("/workspace/") && isDesktopFileExplorerOpen,
|
isWorkspaceExplorerOpen: isWorkspaceRoute && isDesktopFileExplorerOpen,
|
||||||
requestedExplorerWidth: explorerWidth,
|
requestedExplorerWidth: explorerWidth,
|
||||||
viewportWidth,
|
viewportWidth,
|
||||||
});
|
});
|
||||||
const desktopSidebarMounted = chromeEnabled && !isFocusModeEnabled;
|
const desktopSidebarMounted = chromeEnabled && !isWorkspaceFocusModeEnabled;
|
||||||
const desktopSidebarVisible =
|
const desktopSidebarVisible =
|
||||||
!isCompactLayout &&
|
!isCompactLayout &&
|
||||||
desktopSidebarMounted &&
|
desktopSidebarMounted &&
|
||||||
@@ -497,7 +502,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
|
|||||||
const appChromeLayout = resolveDesktopAppChromeLayout({
|
const appChromeLayout = resolveDesktopAppChromeLayout({
|
||||||
desktopSidebarRendered: desktopSidebarVisible,
|
desktopSidebarRendered: desktopSidebarVisible,
|
||||||
hasTopLeftWindowControls,
|
hasTopLeftWindowControls,
|
||||||
sidebarControlsEnabled: chromeEnabled && !isFocusModeEnabled,
|
sidebarControlsEnabled: chromeEnabled && !isWorkspaceFocusModeEnabled,
|
||||||
});
|
});
|
||||||
const sidebarChrome = (
|
const sidebarChrome = (
|
||||||
<SidebarChrome
|
<SidebarChrome
|
||||||
@@ -659,16 +664,23 @@ function DesktopWindowControlsSync({ enabled }: { enabled: boolean }) {
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const surface0 = theme.colors.surface0;
|
const surface0 = theme.colors.surface0;
|
||||||
const foreground = theme.colors.foreground;
|
const foreground = theme.colors.foreground;
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
||||||
|
const liftTrafficLights =
|
||||||
|
getIsElectronRuntimeMac() &&
|
||||||
|
isFocusModeEnabled &&
|
||||||
|
parseHostWorkspaceRouteFromPathname(pathname) !== null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled || isNative) return;
|
if (!enabled || isNative) return;
|
||||||
void updateDesktopWindowControls({
|
void updateDesktopWindowControls({
|
||||||
backgroundColor: surface0,
|
backgroundColor: surface0,
|
||||||
foregroundColor: foreground,
|
foregroundColor: foreground,
|
||||||
|
trafficLightOffsetY: liftTrafficLights ? -5 : 0.5,
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.warn("[DesktopWindow] Failed to update window controls overlay", error);
|
console.warn("[DesktopWindow] Failed to update window controls overlay", error);
|
||||||
});
|
});
|
||||||
}, [enabled, surface0, foreground]);
|
}, [enabled, surface0, foreground, liftTrafficLights]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -987,7 +999,7 @@ const layoutStyles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
windowSidebarToggle: {
|
windowSidebarToggle: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 1,
|
||||||
left: 0,
|
left: 0,
|
||||||
zIndex: 20,
|
zIndex: 20,
|
||||||
height: HEADER_INNER_HEIGHT,
|
height: HEADER_INNER_HEIGHT,
|
||||||
|
|||||||
@@ -45,12 +45,14 @@ function MobileMenuIcon({ color }: { color: string }) {
|
|||||||
|
|
||||||
function SidebarMenuToggleButton({
|
function SidebarMenuToggleButton({
|
||||||
isMobile,
|
isMobile,
|
||||||
|
extraMutedIdleIcon = false,
|
||||||
resolvedStyle,
|
resolvedStyle,
|
||||||
tooltipSide = "right",
|
tooltipSide = "right",
|
||||||
testID = "menu-button",
|
testID = "menu-button",
|
||||||
nativeID = "menu-button",
|
nativeID = "menu-button",
|
||||||
}: Omit<SidebarMenuToggleProps, "style"> & {
|
}: Omit<SidebarMenuToggleProps, "style"> & {
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
extraMutedIdleIcon?: boolean;
|
||||||
resolvedStyle: StyleProp<ViewStyle>;
|
resolvedStyle: StyleProp<ViewStyle>;
|
||||||
}) {
|
}) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
@@ -83,7 +85,12 @@ function SidebarMenuToggleButton({
|
|||||||
accessibilityState={accessibilityState}
|
accessibilityState={accessibilityState}
|
||||||
>
|
>
|
||||||
{({ hovered, pressed }) => {
|
{({ hovered, pressed }) => {
|
||||||
const color = hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted;
|
let color = extraMutedIdleIcon
|
||||||
|
? theme.colors.foregroundExtraMuted
|
||||||
|
: theme.colors.foregroundMuted;
|
||||||
|
if (hovered || pressed) {
|
||||||
|
color = theme.colors.foreground;
|
||||||
|
}
|
||||||
return isMobile ? (
|
return isMobile ? (
|
||||||
<MobileMenuIcon color={color} />
|
<MobileMenuIcon color={color} />
|
||||||
) : (
|
) : (
|
||||||
@@ -121,7 +128,14 @@ export function SidebarMenuToggle({ style, ...props }: SidebarMenuToggleProps =
|
|||||||
|
|
||||||
export function WindowSidebarMenuToggle({ style, ...props }: SidebarMenuToggleProps = {}) {
|
export function WindowSidebarMenuToggle({ style, ...props }: SidebarMenuToggleProps = {}) {
|
||||||
const resolvedStyle = useMemo(() => [styles.leadingToggle, style], [style]);
|
const resolvedStyle = useMemo(() => [styles.leadingToggle, style], [style]);
|
||||||
return <SidebarMenuToggleButton {...props} isMobile={false} resolvedStyle={resolvedStyle} />;
|
return (
|
||||||
|
<SidebarMenuToggleButton
|
||||||
|
{...props}
|
||||||
|
isMobile={false}
|
||||||
|
extraMutedIdleIcon
|
||||||
|
resolvedStyle={resolvedStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MenuHeader({ title, rightContent, borderless }: MenuHeaderProps) {
|
export function MenuHeader({ title, rightContent, borderless }: MenuHeaderProps) {
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ interface SplitContainerProps {
|
|||||||
onReorderTabsInPane: (paneId: string, tabIds: string[]) => void;
|
onReorderTabsInPane: (paneId: string, tabIds: string[]) => void;
|
||||||
renderPaneEmptyState?: () => ReactNode;
|
renderPaneEmptyState?: () => ReactNode;
|
||||||
focusModeEnabled?: boolean;
|
focusModeEnabled?: boolean;
|
||||||
|
onExitFocusMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkspaceTabDragData {
|
interface WorkspaceTabDragData {
|
||||||
@@ -390,6 +391,7 @@ export function SplitContainer({
|
|||||||
onReorderTabsInPane,
|
onReorderTabsInPane,
|
||||||
renderPaneEmptyState = () => null,
|
renderPaneEmptyState = () => null,
|
||||||
focusModeEnabled,
|
focusModeEnabled,
|
||||||
|
onExitFocusMode,
|
||||||
}: SplitContainerProps) {
|
}: SplitContainerProps) {
|
||||||
const inheritedWindowChromeCorners = useWindowChromeCorners();
|
const inheritedWindowChromeCorners = useWindowChromeCorners();
|
||||||
const windowChromeCorners = focusModeEnabled ? inheritedWindowChromeCorners : "none";
|
const windowChromeCorners = focusModeEnabled ? inheritedWindowChromeCorners : "none";
|
||||||
@@ -611,6 +613,8 @@ export function SplitContainer({
|
|||||||
dropPreview={dropPreview}
|
dropPreview={dropPreview}
|
||||||
tabDropPreview={tabDropPreview}
|
tabDropPreview={tabDropPreview}
|
||||||
windowChromeCorners={splitRoot.usesFallbackStrip ? "none" : windowChromeCorners}
|
windowChromeCorners={splitRoot.usesFallbackStrip ? "none" : windowChromeCorners}
|
||||||
|
focusModeEnabled={focusModeEnabled}
|
||||||
|
onExitFocusMode={onExitFocusMode}
|
||||||
/>
|
/>
|
||||||
<DragOverlay dropAnimation={null}>
|
<DragOverlay dropAnimation={null}>
|
||||||
{activeDragTabId ? (
|
{activeDragTabId ? (
|
||||||
@@ -755,6 +759,8 @@ function SplitNodeView({
|
|||||||
dropPreview,
|
dropPreview,
|
||||||
tabDropPreview,
|
tabDropPreview,
|
||||||
windowChromeCorners,
|
windowChromeCorners,
|
||||||
|
focusModeEnabled,
|
||||||
|
onExitFocusMode,
|
||||||
}: SplitNodeViewProps) {
|
}: SplitNodeViewProps) {
|
||||||
const groupId = node.kind === "group" ? node.group.id : null;
|
const groupId = node.kind === "group" ? node.group.id : null;
|
||||||
const groupDirection = node.kind === "group" ? node.group.direction : null;
|
const groupDirection = node.kind === "group" ? node.group.direction : null;
|
||||||
@@ -808,6 +814,8 @@ function SplitNodeView({
|
|||||||
showDropZones={showDropZones}
|
showDropZones={showDropZones}
|
||||||
dropPreview={dropPreview}
|
dropPreview={dropPreview}
|
||||||
tabDropPreview={tabDropPreview}
|
tabDropPreview={tabDropPreview}
|
||||||
|
focusModeEnabled={focusModeEnabled}
|
||||||
|
onExitFocusMode={onExitFocusMode}
|
||||||
/>
|
/>
|
||||||
</WindowChromeRegion>
|
</WindowChromeRegion>
|
||||||
);
|
);
|
||||||
@@ -857,6 +865,8 @@ function SplitNodeView({
|
|||||||
dropPreview={dropPreview}
|
dropPreview={dropPreview}
|
||||||
tabDropPreview={tabDropPreview}
|
tabDropPreview={tabDropPreview}
|
||||||
windowChromeCorners={windowChromeCorners}
|
windowChromeCorners={windowChromeCorners}
|
||||||
|
focusModeEnabled={focusModeEnabled}
|
||||||
|
onExitFocusMode={onExitFocusMode}
|
||||||
/>
|
/>
|
||||||
</SplitGroupChild>
|
</SplitGroupChild>
|
||||||
{index < node.group.children.length - 1 ? (
|
{index < node.group.children.length - 1 ? (
|
||||||
@@ -908,6 +918,8 @@ function SplitPaneView({
|
|||||||
showDropZones,
|
showDropZones,
|
||||||
dropPreview,
|
dropPreview,
|
||||||
tabDropPreview,
|
tabDropPreview,
|
||||||
|
focusModeEnabled,
|
||||||
|
onExitFocusMode,
|
||||||
}: SplitPaneViewProps) {
|
}: SplitPaneViewProps) {
|
||||||
const { theme: _theme } = useUnistyles();
|
const { theme: _theme } = useUnistyles();
|
||||||
const paneRef = useRef<View | null>(null);
|
const paneRef = useRef<View | null>(null);
|
||||||
@@ -1043,6 +1055,8 @@ function SplitPaneView({
|
|||||||
tabDropPreviewIndex={
|
tabDropPreviewIndex={
|
||||||
tabDropPreview?.paneId === pane.id ? tabDropPreview.indicatorIndex : null
|
tabDropPreview?.paneId === pane.id ? tabDropPreview.indicatorIndex : null
|
||||||
}
|
}
|
||||||
|
focusModeEnabled={Boolean(focusModeEnabled)}
|
||||||
|
onExitFocusMode={onExitFocusMode}
|
||||||
/>
|
/>
|
||||||
</WindowChromeSafeArea>
|
</WindowChromeSafeArea>
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export interface DesktopWindowControlsOverlayUpdate {
|
|||||||
height?: number;
|
height?: number;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
foregroundColor?: string;
|
foregroundColor?: string;
|
||||||
|
trafficLightOffsetY?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DesktopWindowBridge {
|
export interface DesktopWindowBridge {
|
||||||
|
|||||||
@@ -34,14 +34,12 @@ export function useKeyboardShortcuts({
|
|||||||
isMobile,
|
isMobile,
|
||||||
toggleAgentList,
|
toggleAgentList,
|
||||||
toggleBothSidebars,
|
toggleBothSidebars,
|
||||||
toggleFocusMode,
|
|
||||||
cycleTheme,
|
cycleTheme,
|
||||||
}: {
|
}: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
toggleAgentList: () => void;
|
toggleAgentList: () => void;
|
||||||
toggleBothSidebars?: () => void;
|
toggleBothSidebars?: () => void;
|
||||||
toggleFocusMode?: () => void;
|
|
||||||
cycleTheme?: () => void;
|
cycleTheme?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -105,7 +103,6 @@ export function useKeyboardShortcuts({
|
|||||||
const callbacksByName: Record<ShortcutCallbackName, (() => void) | undefined> = {
|
const callbacksByName: Record<ShortcutCallbackName, (() => void) | undefined> = {
|
||||||
"toggle-agent-list": toggleAgentList,
|
"toggle-agent-list": toggleAgentList,
|
||||||
"toggle-both-sidebars": toggleBothSidebars,
|
"toggle-both-sidebars": toggleBothSidebars,
|
||||||
"toggle-focus-mode": toggleFocusMode,
|
|
||||||
"cycle-theme": cycleTheme,
|
"cycle-theme": cycleTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -305,6 +302,5 @@ export function useKeyboardShortcuts({
|
|||||||
router,
|
router,
|
||||||
toggleAgentList,
|
toggleAgentList,
|
||||||
toggleBothSidebars,
|
toggleBothSidebars,
|
||||||
toggleFocusMode,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -499,6 +499,7 @@ export const ar: TranslationResources = {
|
|||||||
preparingTerminal: "إعداد علامة التبويب المحطة الطرفية",
|
preparingTerminal: "إعداد علامة التبويب المحطة الطرفية",
|
||||||
preparingTerminalTooltip: "جارٍ تحضير المحطة...",
|
preparingTerminalTooltip: "جارٍ تحضير المحطة...",
|
||||||
newBrowser: "متصفح جديد",
|
newBrowser: "متصفح جديد",
|
||||||
|
exitFocusMode: "إنهاء وضع التركيز",
|
||||||
splitRight: "تقسيم الجزء الأيمن",
|
splitRight: "تقسيم الجزء الأيمن",
|
||||||
splitDown: "تقسيم الجزء لأسفل",
|
splitDown: "تقسيم الجزء لأسفل",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
|||||||
@@ -498,6 +498,7 @@ export const en = {
|
|||||||
preparingTerminal: "Preparing terminal tab",
|
preparingTerminal: "Preparing terminal tab",
|
||||||
preparingTerminalTooltip: "Preparing terminal...",
|
preparingTerminalTooltip: "Preparing terminal...",
|
||||||
newBrowser: "New browser",
|
newBrowser: "New browser",
|
||||||
|
exitFocusMode: "Exit focus mode",
|
||||||
splitRight: "Split pane right",
|
splitRight: "Split pane right",
|
||||||
splitDown: "Split pane down",
|
splitDown: "Split pane down",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
|||||||
@@ -504,6 +504,7 @@ export const es: TranslationResources = {
|
|||||||
preparingTerminal: "Preparando la pestaña del terminal",
|
preparingTerminal: "Preparando la pestaña del terminal",
|
||||||
preparingTerminalTooltip: "Preparando terminal...",
|
preparingTerminalTooltip: "Preparando terminal...",
|
||||||
newBrowser: "Nuevo navegador",
|
newBrowser: "Nuevo navegador",
|
||||||
|
exitFocusMode: "Salir del modo de concentración",
|
||||||
splitRight: "Panel dividido a la derecha",
|
splitRight: "Panel dividido a la derecha",
|
||||||
splitDown: "Dividir panel hacia abajo",
|
splitDown: "Dividir panel hacia abajo",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
|||||||
@@ -503,6 +503,7 @@ export const fr: TranslationResources = {
|
|||||||
preparingTerminal: "Préparation de l'onglet du terminal",
|
preparingTerminal: "Préparation de l'onglet du terminal",
|
||||||
preparingTerminalTooltip: "Préparation du terminal...",
|
preparingTerminalTooltip: "Préparation du terminal...",
|
||||||
newBrowser: "Nouveau navigateur",
|
newBrowser: "Nouveau navigateur",
|
||||||
|
exitFocusMode: "Quitter le mode concentration",
|
||||||
splitRight: "Volet divisé à droite",
|
splitRight: "Volet divisé à droite",
|
||||||
splitDown: "Diviser le volet vers le bas",
|
splitDown: "Diviser le volet vers le bas",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
|||||||
@@ -504,6 +504,7 @@ export const ja: TranslationResources = {
|
|||||||
preparingTerminal: "ターミナルタブを準備中",
|
preparingTerminal: "ターミナルタブを準備中",
|
||||||
preparingTerminalTooltip: "ターミナルを準備中...",
|
preparingTerminalTooltip: "ターミナルを準備中...",
|
||||||
newBrowser: "新しいブラウザ",
|
newBrowser: "新しいブラウザ",
|
||||||
|
exitFocusMode: "フォーカスモードを終了",
|
||||||
splitRight: "右にペインを分割",
|
splitRight: "右にペインを分割",
|
||||||
splitDown: "下にペインを分割",
|
splitDown: "下にペインを分割",
|
||||||
terminalProfilesMenu: "ターミナルプロファイル",
|
terminalProfilesMenu: "ターミナルプロファイル",
|
||||||
|
|||||||
@@ -503,6 +503,7 @@ export const ptBR: TranslationResources = {
|
|||||||
preparingTerminal: "Preparando aba de terminal",
|
preparingTerminal: "Preparando aba de terminal",
|
||||||
preparingTerminalTooltip: "Preparando terminal...",
|
preparingTerminalTooltip: "Preparando terminal...",
|
||||||
newBrowser: "Novo navegador",
|
newBrowser: "Novo navegador",
|
||||||
|
exitFocusMode: "Sair do modo de foco",
|
||||||
splitRight: "Dividir painel à direita",
|
splitRight: "Dividir painel à direita",
|
||||||
splitDown: "Dividir painel abaixo",
|
splitDown: "Dividir painel abaixo",
|
||||||
terminalProfilesMenu: "Perfis de terminal",
|
terminalProfilesMenu: "Perfis de terminal",
|
||||||
|
|||||||
@@ -503,6 +503,7 @@ export const ru: TranslationResources = {
|
|||||||
preparingTerminal: "Подготовка вкладки терминала",
|
preparingTerminal: "Подготовка вкладки терминала",
|
||||||
preparingTerminalTooltip: "Подготовка терминала...",
|
preparingTerminalTooltip: "Подготовка терминала...",
|
||||||
newBrowser: "Новый браузер",
|
newBrowser: "Новый браузер",
|
||||||
|
exitFocusMode: "Выйти из режима фокусировки",
|
||||||
splitRight: "Разделить панель справа",
|
splitRight: "Разделить панель справа",
|
||||||
splitDown: "Разделить панель вниз",
|
splitDown: "Разделить панель вниз",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
|||||||
@@ -499,6 +499,7 @@ export const zhCN: TranslationResources = {
|
|||||||
preparingTerminal: "正在准备 Terminal 标签",
|
preparingTerminal: "正在准备 Terminal 标签",
|
||||||
preparingTerminalTooltip: "正在准备 Terminal...",
|
preparingTerminalTooltip: "正在准备 Terminal...",
|
||||||
newBrowser: "新建浏览器",
|
newBrowser: "新建浏览器",
|
||||||
|
exitFocusMode: "退出专注模式",
|
||||||
splitRight: "向右拆分窗格",
|
splitRight: "向右拆分窗格",
|
||||||
splitDown: "向下拆分窗格",
|
splitDown: "向下拆分窗格",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type KeyboardActionId =
|
|||||||
| "workspace.pane.move-tab.up"
|
| "workspace.pane.move-tab.up"
|
||||||
| "workspace.pane.move-tab.down"
|
| "workspace.pane.move-tab.down"
|
||||||
| "workspace.pane.close"
|
| "workspace.pane.close"
|
||||||
|
| "workspace.focus.toggle"
|
||||||
| "workspace.terminal.new"
|
| "workspace.terminal.new"
|
||||||
| "sidebar.toggle.right"
|
| "sidebar.toggle.right"
|
||||||
| "workspace.new"
|
| "workspace.new"
|
||||||
@@ -57,6 +58,7 @@ export type KeyboardActionDefinition =
|
|||||||
| { id: "workspace.pane.move-tab.up"; scope: KeyboardActionScope }
|
| { id: "workspace.pane.move-tab.up"; scope: KeyboardActionScope }
|
||||||
| { id: "workspace.pane.move-tab.down"; scope: KeyboardActionScope }
|
| { id: "workspace.pane.move-tab.down"; scope: KeyboardActionScope }
|
||||||
| { id: "workspace.pane.close"; scope: KeyboardActionScope }
|
| { id: "workspace.pane.close"; scope: KeyboardActionScope }
|
||||||
|
| { id: "workspace.focus.toggle"; scope: KeyboardActionScope }
|
||||||
| { id: "workspace.terminal.new"; scope: KeyboardActionScope }
|
| { id: "workspace.terminal.new"; scope: KeyboardActionScope }
|
||||||
| { id: "sidebar.toggle.right"; scope: KeyboardActionScope }
|
| { id: "sidebar.toggle.right"; scope: KeyboardActionScope }
|
||||||
| { id: "workspace.new"; scope: KeyboardActionScope }
|
| { id: "workspace.new"; scope: KeyboardActionScope }
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ describe("routeKeyboardShortcut — dispatch passthroughs", () => {
|
|||||||
["workspace.pane.move-tab.up", { id: "workspace.pane.move-tab.up", scope: "workspace" }],
|
["workspace.pane.move-tab.up", { id: "workspace.pane.move-tab.up", scope: "workspace" }],
|
||||||
["workspace.pane.move-tab.down", { id: "workspace.pane.move-tab.down", scope: "workspace" }],
|
["workspace.pane.move-tab.down", { id: "workspace.pane.move-tab.down", scope: "workspace" }],
|
||||||
["workspace.pane.close", { id: "workspace.pane.close", scope: "workspace" }],
|
["workspace.pane.close", { id: "workspace.pane.close", scope: "workspace" }],
|
||||||
|
["view.toggle.focus", { id: "workspace.focus.toggle", scope: "workspace" }],
|
||||||
])("%s → dispatch %j", (action, expected) => {
|
])("%s → dispatch %j", (action, expected) => {
|
||||||
expect(routeKeyboardShortcut({ action, payload: null }, makeCtx())).toEqual({
|
expect(routeKeyboardShortcut({ action, payload: null }, makeCtx())).toEqual({
|
||||||
kind: "dispatch",
|
kind: "dispatch",
|
||||||
@@ -333,7 +334,6 @@ describe("routeKeyboardShortcut — callbacks and pickers", () => {
|
|||||||
it.each([
|
it.each([
|
||||||
["sidebar.toggle.left", "toggle-agent-list"],
|
["sidebar.toggle.left", "toggle-agent-list"],
|
||||||
["sidebar.toggle.both", "toggle-both-sidebars"],
|
["sidebar.toggle.both", "toggle-both-sidebars"],
|
||||||
["view.toggle.focus", "toggle-focus-mode"],
|
|
||||||
["theme.cycle", "cycle-theme"],
|
["theme.cycle", "cycle-theme"],
|
||||||
] as const)("%s → callback %s", (action, name) => {
|
] as const)("%s → callback %s", (action, name) => {
|
||||||
expect(routeKeyboardShortcut({ action, payload: null }, makeCtx())).toEqual<ShortcutAction>({
|
expect(routeKeyboardShortcut({ action, payload: null }, makeCtx())).toEqual<ShortcutAction>({
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ export interface ShortcutRoutingInput {
|
|||||||
payload: KeyboardShortcutPayload;
|
payload: KeyboardShortcutPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ShortcutCallbackName =
|
export type ShortcutCallbackName = "toggle-agent-list" | "toggle-both-sidebars" | "cycle-theme";
|
||||||
| "toggle-agent-list"
|
|
||||||
| "toggle-both-sidebars"
|
|
||||||
| "toggle-focus-mode"
|
|
||||||
| "cycle-theme";
|
|
||||||
|
|
||||||
export type ShortcutAction =
|
export type ShortcutAction =
|
||||||
| { kind: "none" }
|
| { kind: "none" }
|
||||||
@@ -63,12 +59,12 @@ const PASSTHROUGH_DISPATCH: Record<string, KeyboardActionDefinition> = {
|
|||||||
"workspace.pane.move-tab.up": { id: "workspace.pane.move-tab.up", scope: "workspace" },
|
"workspace.pane.move-tab.up": { id: "workspace.pane.move-tab.up", scope: "workspace" },
|
||||||
"workspace.pane.move-tab.down": { id: "workspace.pane.move-tab.down", scope: "workspace" },
|
"workspace.pane.move-tab.down": { id: "workspace.pane.move-tab.down", scope: "workspace" },
|
||||||
"workspace.pane.close": { id: "workspace.pane.close", scope: "workspace" },
|
"workspace.pane.close": { id: "workspace.pane.close", scope: "workspace" },
|
||||||
|
"view.toggle.focus": { id: "workspace.focus.toggle", scope: "workspace" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const SIMPLE_CALLBACKS: Record<string, ShortcutCallbackName> = {
|
const SIMPLE_CALLBACKS: Record<string, ShortcutCallbackName> = {
|
||||||
"sidebar.toggle.left": "toggle-agent-list",
|
"sidebar.toggle.left": "toggle-agent-list",
|
||||||
"sidebar.toggle.both": "toggle-both-sidebars",
|
"sidebar.toggle.both": "toggle-both-sidebars",
|
||||||
"view.toggle.focus": "toggle-focus-mode",
|
|
||||||
"theme.cycle": "cycle-theme",
|
"theme.cycle": "cycle-theme",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1081,7 +1081,12 @@ function SettingsSidebar({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={outerContainerStyle} testID="settings-sidebar">
|
<View
|
||||||
|
accessibilityLabel={t("settings.title")}
|
||||||
|
role="navigation"
|
||||||
|
style={outerContainerStyle}
|
||||||
|
testID="settings-sidebar"
|
||||||
|
>
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<View style={innerContainerStyle}>
|
<View style={innerContainerStyle}>
|
||||||
<View style={sidebarStyles.sidebarDragArea}>
|
<View style={sidebarStyles.sidebarDragArea}>
|
||||||
|
|||||||
@@ -438,6 +438,8 @@ interface WorkspaceDesktopTabsRowProps {
|
|||||||
activeDragTabId?: string | null;
|
activeDragTabId?: string | null;
|
||||||
tabDropPreviewIndex?: number | null;
|
tabDropPreviewIndex?: number | null;
|
||||||
showPaneSplitActions?: boolean;
|
showPaneSplitActions?: boolean;
|
||||||
|
focusModeEnabled: boolean;
|
||||||
|
onExitFocusMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFallbackTabLabel(
|
function getFallbackTabLabel(
|
||||||
@@ -758,15 +760,19 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
activeDragTabId = null,
|
activeDragTabId = null,
|
||||||
tabDropPreviewIndex = null,
|
tabDropPreviewIndex = null,
|
||||||
showPaneSplitActions = true,
|
showPaneSplitActions = true,
|
||||||
|
focusModeEnabled,
|
||||||
|
onExitFocusMode,
|
||||||
}: WorkspaceDesktopTabsRowProps) {
|
}: WorkspaceDesktopTabsRowProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
||||||
|
const focusModeKeys = useShortcutKeys("toggle-focus");
|
||||||
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
|
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
|
||||||
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
|
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
|
||||||
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
||||||
const [tabsActionsWidth, setTabsActionsWidth] = useState<number>(0);
|
const [tabsActionsWidth, setTabsActionsWidth] = useState<number>(0);
|
||||||
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
|
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
|
||||||
|
const [exitFocusModeWidth, setExitFocusModeWidth] = useState<number>(0);
|
||||||
|
|
||||||
const handleTabsContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
const handleTabsContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||||
updateMeasuredWidth(setTabsContainerWidth, event);
|
updateMeasuredWidth(setTabsContainerWidth, event);
|
||||||
@@ -780,12 +786,18 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
updateMeasuredWidth(setInlineAddButtonWidth, event);
|
updateMeasuredWidth(setInlineAddButtonWidth, event);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleExitFocusModeLayout = useCallback((event: LayoutChangeEvent) => {
|
||||||
|
updateMeasuredWidth(setExitFocusModeWidth, event);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const layoutMetrics = useMemo(
|
const layoutMetrics = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
rowHorizontalInset: 0,
|
rowHorizontalInset: 0,
|
||||||
actionsReservedWidth: Math.max(
|
actionsReservedWidth: Math.max(
|
||||||
0,
|
0,
|
||||||
tabsActionsWidth + (inlineAddButtonWidth || DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH),
|
tabsActionsWidth +
|
||||||
|
(inlineAddButtonWidth || DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH) +
|
||||||
|
(focusModeEnabled ? exitFocusModeWidth : 0),
|
||||||
),
|
),
|
||||||
rowPaddingHorizontal: 0,
|
rowPaddingHorizontal: 0,
|
||||||
tabGap: 0,
|
tabGap: 0,
|
||||||
@@ -795,7 +807,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
estimatedCharWidth: 7,
|
estimatedCharWidth: 7,
|
||||||
closeButtonWidth: 22,
|
closeButtonWidth: 22,
|
||||||
}),
|
}),
|
||||||
[inlineAddButtonWidth, tabsActionsWidth],
|
[exitFocusModeWidth, focusModeEnabled, inlineAddButtonWidth, tabsActionsWidth],
|
||||||
);
|
);
|
||||||
|
|
||||||
const fallbackTabLabels = useMemo(
|
const fallbackTabLabels = useMemo(
|
||||||
@@ -968,6 +980,31 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
testID="workspace-tabs-row"
|
testID="workspace-tabs-row"
|
||||||
onLayout={handleTabsContainerLayout}
|
onLayout={handleTabsContainerLayout}
|
||||||
>
|
>
|
||||||
|
{focusModeEnabled ? (
|
||||||
|
<View style={styles.exitFocusModeSlot} onLayout={handleExitFocusModeLayout}>
|
||||||
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
|
<TooltipTrigger
|
||||||
|
testID="workspace-exit-focus-mode"
|
||||||
|
onPress={onExitFocusMode}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t("workspace.tabs.actions.exitFocusMode")}
|
||||||
|
style={inlineAddActionButtonStyle}
|
||||||
|
>
|
||||||
|
<ThemedX size={14} uniProps={mutedColorMapping} />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" align="center" offset={8}>
|
||||||
|
<View style={styles.newTabTooltipRow}>
|
||||||
|
<Text style={styles.newTabTooltipText}>
|
||||||
|
{t("workspace.tabs.actions.exitFocusMode")}
|
||||||
|
</Text>
|
||||||
|
{focusModeKeys ? (
|
||||||
|
<Shortcut chord={focusModeKeys} style={styles.newTabTooltipShortcut} />
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
scrollEnabled={layout.requiresHorizontalScrollFallback}
|
scrollEnabled={layout.requiresHorizontalScrollFallback}
|
||||||
@@ -1184,6 +1221,14 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingHorizontal: theme.spacing[2],
|
paddingHorizontal: theme.spacing[2],
|
||||||
},
|
},
|
||||||
|
exitFocusModeSlot: {
|
||||||
|
alignSelf: "stretch",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: theme.spacing[1],
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: theme.colors.border,
|
||||||
|
},
|
||||||
inlineAddButton: {
|
inlineAddButton: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -266,6 +266,9 @@ const ThemedDynamicProviderIcon = withUnistyles(DynamicProviderIcon);
|
|||||||
|
|
||||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
const extraMutedColorMapping = (theme: Theme) => ({
|
||||||
|
color: theme.colors.foregroundExtraMuted,
|
||||||
|
});
|
||||||
|
|
||||||
const sourceControlPanelStrokeWidth15 = { strokeWidth: 1.5 };
|
const sourceControlPanelStrokeWidth15 = { strokeWidth: 1.5 };
|
||||||
|
|
||||||
@@ -1712,6 +1715,7 @@ function WorkspaceScreenContent({
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
||||||
|
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
|
||||||
|
|
||||||
const normalizedServerId = useMemo(() => trimNonEmpty(decodeSegment(serverId)) ?? "", [serverId]);
|
const normalizedServerId = useMemo(() => trimNonEmpty(decodeSegment(serverId)) ?? "", [serverId]);
|
||||||
|
|
||||||
@@ -2966,6 +2970,11 @@ function WorkspaceScreenContent({
|
|||||||
|
|
||||||
const handleWorkspacePaneAction = useCallback(
|
const handleWorkspacePaneAction = useCallback(
|
||||||
(action: KeyboardActionDefinition): boolean => {
|
(action: KeyboardActionDefinition): boolean => {
|
||||||
|
if (action.id === "workspace.focus.toggle") {
|
||||||
|
toggleFocusMode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!persistenceKey || !workspaceLayout) {
|
if (!persistenceKey || !workspaceLayout) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3039,6 +3048,7 @@ function WorkspaceScreenContent({
|
|||||||
persistenceKey,
|
persistenceKey,
|
||||||
focusedPaneTabState.activeTabId,
|
focusedPaneTabState.activeTabId,
|
||||||
focusedPaneTabState.pane,
|
focusedPaneTabState.pane,
|
||||||
|
toggleFocusMode,
|
||||||
workspaceLayout,
|
workspaceLayout,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -3072,6 +3082,7 @@ function WorkspaceScreenContent({
|
|||||||
"workspace.pane.move-tab.up",
|
"workspace.pane.move-tab.up",
|
||||||
"workspace.pane.move-tab.down",
|
"workspace.pane.move-tab.down",
|
||||||
"workspace.pane.close",
|
"workspace.pane.close",
|
||||||
|
"workspace.focus.toggle",
|
||||||
] as const,
|
] as const,
|
||||||
enabled: Boolean(isRouteFocused && normalizedServerId && normalizedWorkspaceId),
|
enabled: Boolean(isRouteFocused && normalizedServerId && normalizedWorkspaceId),
|
||||||
priority: 100,
|
priority: 100,
|
||||||
@@ -3366,8 +3377,8 @@ function WorkspaceScreenContent({
|
|||||||
style={explorerToggleStyle}
|
style={explorerToggleStyle}
|
||||||
>
|
>
|
||||||
{({ hovered, pressed }) => {
|
{({ hovered, pressed }) => {
|
||||||
const active = isExplorerOpen || hovered || pressed;
|
const active = hovered || pressed;
|
||||||
const colorMapping = active ? foregroundColorMapping : mutedColorMapping;
|
const colorMapping = active ? foregroundColorMapping : extraMutedColorMapping;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ThemedSourceControlPanelIcon size={16} uniProps={colorMapping} />
|
<ThemedSourceControlPanelIcon size={16} uniProps={colorMapping} />
|
||||||
@@ -3412,9 +3423,9 @@ function WorkspaceScreenContent({
|
|||||||
accessibilityLabel={explorerToggleLabel}
|
accessibilityLabel={explorerToggleLabel}
|
||||||
accessibilityState={explorerToggleAccessibilityState}
|
accessibilityState={explorerToggleAccessibilityState}
|
||||||
>
|
>
|
||||||
{({ hovered }) => {
|
{({ hovered, pressed }) => {
|
||||||
const colorMapping =
|
const colorMapping =
|
||||||
isExplorerOpen || hovered ? foregroundColorMapping : mutedColorMapping;
|
hovered || pressed ? foregroundColorMapping : extraMutedColorMapping;
|
||||||
return <ThemedPanelRight size={16} uniProps={colorMapping} />;
|
return <ThemedPanelRight size={16} uniProps={colorMapping} />;
|
||||||
}}
|
}}
|
||||||
</HeaderToggleButton>
|
</HeaderToggleButton>
|
||||||
@@ -3502,6 +3513,7 @@ function WorkspaceScreenContent({
|
|||||||
<SplitContainer
|
<SplitContainer
|
||||||
layout={workspaceLayout}
|
layout={workspaceLayout}
|
||||||
focusModeEnabled={desktopFocusModeEnabled}
|
focusModeEnabled={desktopFocusModeEnabled}
|
||||||
|
onExitFocusMode={toggleFocusMode}
|
||||||
workspaceKey={persistenceKey}
|
workspaceKey={persistenceKey}
|
||||||
normalizedServerId={normalizedServerId}
|
normalizedServerId={normalizedServerId}
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
@@ -3539,6 +3551,7 @@ function WorkspaceScreenContent({
|
|||||||
workspaceLayout,
|
workspaceLayout,
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
desktopFocusModeEnabled,
|
desktopFocusModeEnabled,
|
||||||
|
toggleFocusMode,
|
||||||
normalizedServerId,
|
normalizedServerId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
isRouteFocused,
|
isRouteFocused,
|
||||||
@@ -3668,6 +3681,8 @@ function WorkspaceScreenContent({
|
|||||||
onSplitRight={noop}
|
onSplitRight={noop}
|
||||||
onSplitDown={noop}
|
onSplitDown={noop}
|
||||||
showPaneSplitActions={false}
|
showPaneSplitActions={false}
|
||||||
|
focusModeEnabled={desktopFocusModeEnabled}
|
||||||
|
onExitFocusMode={toggleFocusMode}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ const lightSemanticColors = {
|
|||||||
// Text
|
// Text
|
||||||
foreground: "#1a1a1e",
|
foreground: "#1a1a1e",
|
||||||
foregroundMuted: "#71717a",
|
foregroundMuted: "#71717a",
|
||||||
|
foregroundExtraMuted: "#a1a1aa",
|
||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
scrollbarHandle: "#3f3f46", // zinc-700
|
scrollbarHandle: "#3f3f46", // zinc-700
|
||||||
@@ -231,6 +232,7 @@ interface DarkThemeConfig {
|
|||||||
surfaceSidebar: string;
|
surfaceSidebar: string;
|
||||||
surfaceSidebarHover: string;
|
surfaceSidebarHover: string;
|
||||||
foregroundMuted: string;
|
foregroundMuted: string;
|
||||||
|
foregroundExtraMuted: string;
|
||||||
scrollbarHandle: string;
|
scrollbarHandle: string;
|
||||||
border: string;
|
border: string;
|
||||||
borderAccent: string;
|
borderAccent: string;
|
||||||
@@ -271,6 +273,7 @@ function buildDarkSemanticColors(tint: DarkThemeConfig) {
|
|||||||
|
|
||||||
foreground: "#fafafa",
|
foreground: "#fafafa",
|
||||||
foregroundMuted: tint.foregroundMuted,
|
foregroundMuted: tint.foregroundMuted,
|
||||||
|
foregroundExtraMuted: tint.foregroundExtraMuted,
|
||||||
|
|
||||||
scrollbarHandle: tint.scrollbarHandle,
|
scrollbarHandle: tint.scrollbarHandle,
|
||||||
|
|
||||||
@@ -332,6 +335,7 @@ const paseoDarkColors = buildDarkSemanticColors({
|
|||||||
surfaceSidebar: "#141716",
|
surfaceSidebar: "#141716",
|
||||||
surfaceSidebarHover: "#1c1f1e",
|
surfaceSidebarHover: "#1c1f1e",
|
||||||
foregroundMuted: "#A1A5A4",
|
foregroundMuted: "#A1A5A4",
|
||||||
|
foregroundExtraMuted: "#717574",
|
||||||
scrollbarHandle: "#717574",
|
scrollbarHandle: "#717574",
|
||||||
border: "#252B2A",
|
border: "#252B2A",
|
||||||
borderAccent: "#2F3534",
|
borderAccent: "#2F3534",
|
||||||
@@ -351,6 +355,7 @@ const zincDarkColors = buildDarkSemanticColors({
|
|||||||
surfaceSidebar: "#131316",
|
surfaceSidebar: "#131316",
|
||||||
surfaceSidebarHover: "#1b1b1e",
|
surfaceSidebarHover: "#1b1b1e",
|
||||||
foregroundMuted: "#a1a1aa",
|
foregroundMuted: "#a1a1aa",
|
||||||
|
foregroundExtraMuted: "#71717a",
|
||||||
scrollbarHandle: "#71717a",
|
scrollbarHandle: "#71717a",
|
||||||
border: "#27272a",
|
border: "#27272a",
|
||||||
borderAccent: "#303036",
|
borderAccent: "#303036",
|
||||||
@@ -371,6 +376,7 @@ const midnightDarkColors = buildDarkSemanticColors({
|
|||||||
surfaceSidebar: "#121420",
|
surfaceSidebar: "#121420",
|
||||||
surfaceSidebarHover: "#1a1c28",
|
surfaceSidebarHover: "#1a1c28",
|
||||||
foregroundMuted: "#9a9db0",
|
foregroundMuted: "#9a9db0",
|
||||||
|
foregroundExtraMuted: "#6b6e82",
|
||||||
scrollbarHandle: "#6b6e82",
|
scrollbarHandle: "#6b6e82",
|
||||||
border: "#242636",
|
border: "#242636",
|
||||||
borderAccent: "#2e3040",
|
borderAccent: "#2e3040",
|
||||||
@@ -390,6 +396,7 @@ const claudeDarkColors = buildDarkSemanticColors({
|
|||||||
surfaceSidebar: "#1a1918",
|
surfaceSidebar: "#1a1918",
|
||||||
surfaceSidebarHover: "#222120",
|
surfaceSidebarHover: "#222120",
|
||||||
foregroundMuted: "#ada9a5",
|
foregroundMuted: "#ada9a5",
|
||||||
|
foregroundExtraMuted: "#78746f",
|
||||||
scrollbarHandle: "#78746f",
|
scrollbarHandle: "#78746f",
|
||||||
border: "#2c2a27",
|
border: "#2c2a27",
|
||||||
borderAccent: "#36332f",
|
borderAccent: "#36332f",
|
||||||
@@ -409,6 +416,7 @@ const ghosttyDarkColors = buildDarkSemanticColors({
|
|||||||
surfaceSidebar: "#21252d",
|
surfaceSidebar: "#21252d",
|
||||||
surfaceSidebarHover: "#292d36",
|
surfaceSidebarHover: "#292d36",
|
||||||
foregroundMuted: "#c8ccd8",
|
foregroundMuted: "#c8ccd8",
|
||||||
|
foregroundExtraMuted: "#a0a4b2",
|
||||||
scrollbarHandle: "#a0a4b2",
|
scrollbarHandle: "#a0a4b2",
|
||||||
border: "#353a47",
|
border: "#353a47",
|
||||||
borderAccent: "#3f4454",
|
borderAccent: "#3f4454",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ contextBridge.exposeInMainWorld("paseoDesktop", {
|
|||||||
height?: number;
|
height?: number;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
foregroundColor?: string;
|
foregroundColor?: string;
|
||||||
|
trafficLightOffsetY?: number;
|
||||||
}) => ipcRenderer.invoke("paseo:window:updateWindowControls", update),
|
}) => ipcRenderer.invoke("paseo:window:updateWindowControls", update),
|
||||||
onResized: (handler: EventHandler): (() => void) => {
|
onResized: (handler: EventHandler): (() => void) => {
|
||||||
const listener = (_ipcEvent: Electron.IpcRendererEvent, payload: unknown) => {
|
const listener = (_ipcEvent: Electron.IpcRendererEvent, payload: unknown) => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
applyMacWindowControlsUpdate,
|
||||||
applyWindowControlsOverlayUpdate,
|
applyWindowControlsOverlayUpdate,
|
||||||
createWindowControlsOverlayState,
|
createWindowControlsOverlayState,
|
||||||
DEFAULT_WINDOW_HEIGHT,
|
DEFAULT_WINDOW_HEIGHT,
|
||||||
@@ -70,10 +71,12 @@ describe("window-manager", () => {
|
|||||||
readWindowControlsOverlayUpdate({
|
readWindowControlsOverlayUpdate({
|
||||||
height: 48,
|
height: 48,
|
||||||
backgroundColor: "#181B1A",
|
backgroundColor: "#181B1A",
|
||||||
|
trafficLightOffsetY: -5,
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
height: 48,
|
height: 48,
|
||||||
backgroundColor: "#181B1A",
|
backgroundColor: "#181B1A",
|
||||||
|
trafficLightOffsetY: -5,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,6 +85,13 @@ describe("window-manager", () => {
|
|||||||
expect(readWindowControlsOverlayUpdate({})).toBeNull();
|
expect(readWindowControlsOverlayUpdate({})).toBeNull();
|
||||||
expect(readWindowControlsOverlayUpdate({ height: 0 })).toBeNull();
|
expect(readWindowControlsOverlayUpdate({ height: 0 })).toBeNull();
|
||||||
expect(readWindowControlsOverlayUpdate({ backgroundColor: 12 })).toBeNull();
|
expect(readWindowControlsOverlayUpdate({ backgroundColor: 12 })).toBeNull();
|
||||||
|
expect(readWindowControlsOverlayUpdate({ trafficLightOffsetY: -11 })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves fractional traffic-light offsets", () => {
|
||||||
|
expect(readWindowControlsOverlayUpdate({ trafficLightOffsetY: 1.5 })).toEqual({
|
||||||
|
trafficLightOffsetY: 1.5,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -139,6 +149,24 @@ describe("window-manager", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("applyMacWindowControlsUpdate", () => {
|
||||||
|
it("uses the focus and normal traffic-light positions", () => {
|
||||||
|
const setWindowButtonPosition = vi.fn();
|
||||||
|
|
||||||
|
applyMacWindowControlsUpdate({
|
||||||
|
win: { setWindowButtonPosition },
|
||||||
|
update: { trafficLightOffsetY: -5 },
|
||||||
|
});
|
||||||
|
applyMacWindowControlsUpdate({
|
||||||
|
win: { setWindowButtonPosition },
|
||||||
|
update: { trafficLightOffsetY: 0.5 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setWindowButtonPosition).toHaveBeenNthCalledWith(1, { x: 16, y: 9 });
|
||||||
|
expect(setWindowButtonPosition).toHaveBeenNthCalledWith(2, { x: 16, y: 14.5 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getMainWindowChromeOptions", () => {
|
describe("getMainWindowChromeOptions", () => {
|
||||||
it("uses frameless hidden title bars with overlay on windows", () => {
|
it("uses frameless hidden title bars with overlay on windows", () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
import type { WindowState, WindowStateStore } from "../settings/window-state.js";
|
import type { WindowState, WindowStateStore } from "../settings/window-state.js";
|
||||||
|
|
||||||
const WINDOW_STATE_SAVE_DEBOUNCE_MS = 400;
|
const WINDOW_STATE_SAVE_DEBOUNCE_MS = 400;
|
||||||
|
const MAC_TRAFFIC_LIGHT_POSITION = { x: 16, y: 14 } as const;
|
||||||
|
const MAX_TRAFFIC_LIGHT_OFFSET_Y = 10;
|
||||||
|
|
||||||
export function readBadgeCount(input: unknown): number {
|
export function readBadgeCount(input: unknown): number {
|
||||||
if (typeof input !== "number" || !Number.isSafeInteger(input) || input < 0) {
|
if (typeof input !== "number" || !Number.isSafeInteger(input) || input < 0) {
|
||||||
@@ -27,6 +29,7 @@ export interface WindowControlsOverlayUpdate {
|
|||||||
height?: number;
|
height?: number;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
foregroundColor?: string;
|
foregroundColor?: string;
|
||||||
|
trafficLightOffsetY?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WindowControlsOverlayState {
|
export interface WindowControlsOverlayState {
|
||||||
@@ -79,7 +82,7 @@ export function getMainWindowChromeOptions(input: {
|
|||||||
return {
|
return {
|
||||||
titleBarStyle: "hidden",
|
titleBarStyle: "hidden",
|
||||||
titleBarOverlay: true,
|
titleBarOverlay: true,
|
||||||
trafficLightPosition: { x: 16, y: 14 },
|
trafficLightPosition: MAC_TRAFFIC_LIGHT_POSITION,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +131,14 @@ function readOverlayColor(input: unknown): string | null {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readTrafficLightOffsetY(input: unknown): number | null {
|
||||||
|
if (typeof input !== "number" || !Number.isFinite(input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.abs(input) <= MAX_TRAFFIC_LIGHT_OFFSET_Y ? input : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function readWindowControlsOverlayUpdate(
|
export function readWindowControlsOverlayUpdate(
|
||||||
input: unknown,
|
input: unknown,
|
||||||
): WindowControlsOverlayUpdate | null {
|
): WindowControlsOverlayUpdate | null {
|
||||||
@@ -139,8 +150,14 @@ export function readWindowControlsOverlayUpdate(
|
|||||||
const height = readFiniteOverlayHeight(candidate.height);
|
const height = readFiniteOverlayHeight(candidate.height);
|
||||||
const backgroundColor = readOverlayColor(candidate.backgroundColor);
|
const backgroundColor = readOverlayColor(candidate.backgroundColor);
|
||||||
const foregroundColor = readOverlayColor(candidate.foregroundColor);
|
const foregroundColor = readOverlayColor(candidate.foregroundColor);
|
||||||
|
const trafficLightOffsetY = readTrafficLightOffsetY(candidate.trafficLightOffsetY);
|
||||||
|
|
||||||
if (height === null && backgroundColor === null && foregroundColor === null) {
|
if (
|
||||||
|
height === null &&
|
||||||
|
backgroundColor === null &&
|
||||||
|
foregroundColor === null &&
|
||||||
|
trafficLightOffsetY === null
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +165,7 @@ export function readWindowControlsOverlayUpdate(
|
|||||||
...(height !== null ? { height } : {}),
|
...(height !== null ? { height } : {}),
|
||||||
...(backgroundColor !== null ? { backgroundColor } : {}),
|
...(backgroundColor !== null ? { backgroundColor } : {}),
|
||||||
...(foregroundColor !== null ? { foregroundColor } : {}),
|
...(foregroundColor !== null ? { foregroundColor } : {}),
|
||||||
|
...(trafficLightOffsetY !== null ? { trafficLightOffsetY } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +194,20 @@ export function applyWindowControlsOverlayUpdate(input: {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyMacWindowControlsUpdate(input: {
|
||||||
|
win: Pick<BrowserWindow, "setWindowButtonPosition">;
|
||||||
|
update: WindowControlsOverlayUpdate;
|
||||||
|
}): void {
|
||||||
|
if (input.update.trafficLightOffsetY === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.win.setWindowButtonPosition({
|
||||||
|
x: MAC_TRAFFIC_LIGHT_POSITION.x,
|
||||||
|
y: MAC_TRAFFIC_LIGHT_POSITION.y + input.update.trafficLightOffsetY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function registerWindowManager(): void {
|
export function registerWindowManager(): void {
|
||||||
const overlayStateByWindow = new WeakMap<BrowserWindow, WindowControlsOverlayState>();
|
const overlayStateByWindow = new WeakMap<BrowserWindow, WindowControlsOverlayState>();
|
||||||
|
|
||||||
@@ -230,6 +262,7 @@ export function registerWindowManager(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
|
applyMacWindowControlsUpdate({ win, update: nextUpdate });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user