feat(ui): full-height left sidebar with decoupled header layout
Rework the desktop layout so the left sidebar spans the full window height and reads as a distinct column, separate from the header. - Persistent top-left controls (sidebar toggle + project actions) as a single overlay that no longer migrates between header and sidebar, so it stays put while panels animate. Drag regions carve out only under the buttons so the strip stays draggable and the buttons clickable. - Header sits in the content column (bg-background); the divider lives on the chat content (border-t) so it doesn't run between header and right sidebar. Right sidebar gets a top border + small content padding. - Drop the rounded chat card and corner masks for flat 1px borders. - Right sidebar, context panel and git/files views inherit the background color instead of bg-sidebar. - Remove hover background from Changes/Staged rows. - Mini-chat header: borderless, bg-background.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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 { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||
|
||||
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 shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
||||
const projectActionsContext = useProjectActionsContext();
|
||||
const clusterRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const toggleShortcut = formatShortcutForDisplay(getEffectiveShortcutCombo('toggle_sidebar', shortcutOverrides));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const node = clusterRef.current;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const publishWidth = () => {
|
||||
const width = node.getBoundingClientRect().width;
|
||||
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">
|
||||
<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}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user