Files
openchamber/packages/ui/src/components/layout/TitlebarLeftControls.tsx
T
Serhii Dziupin 484fe8bc18 fix(ui): move window controls position setting to Appearance (#2406)
* fix(ui): move window controls position setting to Appearance

Keep desktop chrome layout with other visual settings; General retains
startup, tray, network, and access controls. Update settings search page
mapping so the control still highlights correctly.

Authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(ui): align Linux window controls and stop actions overlap

Size left-side window controls to match the titlebar icon row and drop
negative margins so the reserved titlebar width stays accurate. This keeps
the project-actions chevron from colliding with the New session title.

Authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(ui): stack window-controls description above chips

Show the Appearance window-controls helper copy full-width under the
section title, with Auto/Left/Right chips beneath it.

Authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

---------
Authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-07-24 21:27:38 +03:00

142 lines
5.8 KiB
TypeScript

import React from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from '@/components/icon/Icon';
import { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/useUIStore';
import { useI18n } from '@/lib/i18n';
import { useProjectActionsContext } from '@/hooks/useProjectActionsContext';
import { ProjectActionsButton } from '@/components/layout/ProjectActionsButton';
import { WindowsWindowControls } from '@/components/desktop/WindowsWindowControls';
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
import { invokeDesktop } from '@/lib/desktop';
import { useDesktopWindowControlsLayout } from '@/hooks/useDesktopWindowControlsLayout';
const ICON_BUTTON_CLASS =
'app-region-no-drag inline-flex h-8 w-8 items-center justify-center gap-2 rounded-md typography-ui-label font-medium text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary hover:bg-interactive-hover transition-colors';
/**
* Persistent top-left titlebar controls (sidebar toggle + project actions).
*
* Rendered exactly once as an absolutely-positioned overlay above both the
* sidebar and the header, so the buttons never migrate / re-mount between the
* two while the sidebar animates open or closed — the panels slide *underneath*
* a fixed control cluster instead. Its height tracks `--oc-header-height` and
* its left padding clears the OS window controls via `--oc-titlebar-left-inset`.
* The cluster's measured width is published as `--oc-titlebar-controls-width`
* so the header can reserve matching space when the sidebar is collapsed.
*/
export const TitlebarLeftControls: React.FC = () => {
const { t } = useI18n();
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const projectActionsContext = useProjectActionsContext();
const clusterRef = React.useRef<HTMLDivElement | null>(null);
const toggleShortcut = formatShortcutForDisplay(getEffectiveShortcutCombo('toggle_sidebar', shortcutOverrides));
const { usesFramelessChrome, side: windowControlsSide } = useDesktopWindowControlsLayout();
const handleOpenWindowsAppMenu = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
void invokeDesktop('desktop_show_app_menu', {
x: rect.left,
y: rect.bottom,
}).catch((error) => {
console.warn('[titlebar] failed to open app menu', error);
});
}, []);
React.useEffect(() => {
if (typeof document === 'undefined') {
return;
}
const node = clusterRef.current;
if (!node) {
return;
}
const publishWidth = () => {
// Prefer scrollWidth so negative child margins / overflow cannot under-report
// the space the overlay actually occupies over the header.
const width = Math.max(node.getBoundingClientRect().width, node.scrollWidth);
document.documentElement.style.setProperty('--oc-titlebar-controls-width', `${Math.round(width)}px`);
};
publishWidth();
if (typeof ResizeObserver === 'undefined') {
return;
}
const observer = new ResizeObserver(publishWidth);
observer.observe(node);
return () => {
observer.disconnect();
};
}, []);
return (
// The overlay is a CSS no-drag zone so its buttons stay clickable. The
// header / sidebar strip beneath carve a matching no-drag region under it
// and remain drag regions everywhere else, so window dragging still works
// in the empty parts of the strip.
<div
className="app-region-no-drag absolute left-0 top-0 z-30 flex select-none items-center pr-2"
style={{
height: 'var(--oc-header-height, 3rem)',
paddingLeft: 'var(--oc-titlebar-left-inset, 0.75rem)',
}}
>
<div ref={clusterRef} className="flex items-center gap-2">
{usesFramelessChrome && windowControlsSide === 'left' ? (
<WindowsWindowControls visible position="left" />
) : null}
{usesFramelessChrome ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleOpenWindowsAppMenu}
aria-label={t('header.actions.openAppMenuAria')}
className={cn(ICON_BUTTON_CLASS, 'shrink-0')}
>
<Icon name="menu-2" className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>{t('header.actions.openAppMenu')}</p>
</TooltipContent>
</Tooltip>
) : null}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={toggleSidebar}
aria-label={t('header.actions.openSessionsAria')}
className={cn(ICON_BUTTON_CLASS, 'shrink-0')}
>
<Icon name="layout-left" className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>{t('header.actions.openSessionsWithShortcut', { shortcut: toggleShortcut })}</p>
</TooltipContent>
</Tooltip>
{projectActionsContext ? (
<ProjectActionsButton
projectRef={projectActionsContext.projectRef}
directory={projectActionsContext.directory}
// While the sidebar is open the controls sit over the frosted
// sidebar — let the pill share its translucency instead of painting
// an opaque surface (handled under [data-oc-vibrancy] in CSS).
className={isSidebarOpen ? 'oc-vibrancy-pill' : undefined}
/>
) : null}
</div>
</div>
);
};