Align Code navigation and branding with workspace header rows

This commit is contained in:
鲨鱼辣椒
2026-09-30 00:39:24 +08:00
parent 9d47cd2146
commit 883ae1d788
10 changed files with 169 additions and 50 deletions

View File

@@ -11,7 +11,6 @@ import {
FolderKanban,
Gamepad2,
LogOut,
PanelLeftClose,
Plus,
RadioTower,
Settings as SettingsIcon,
@@ -91,7 +90,6 @@ export function Sidebar({
}: SidebarProps = {}) {
const { t } = useTranslation('common');
const storedSidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
const sidebarWidth = useSettingsStore((state) => normalizeClassroomSidebarWidth(state.classroomSidebarWidth));
const sidebarPinnedCollapsed = !forceExpanded && storedSidebarCollapsed;
const sidebarCollapsed = sidebarPinnedCollapsed && !sidebarPeekOpen;
@@ -415,17 +413,8 @@ export function Sidebar({
)}
>
<div className="min-w-0 w-full">
{workspaceLayout && <div className="no-drag flex h-10 min-w-0 items-center gap-1" data-testid="classroom-sidebar-navigation">
{workspaceLayout && !['darwin', 'win32'].includes(window.electron?.platform ?? '') && <div className="no-drag flex h-10 min-w-0 items-center gap-1" data-testid="classroom-sidebar-navigation">
<ModuleSwitcher sidebarCollapsed={sidebarCollapsed} toolbar iconOnly />
{!sidebarPinnedCollapsed && ['darwin', 'win32'].includes(window.electron?.platform ?? '') && <button
type="button"
data-testid="titlebar-sidebar-toggle"
aria-label="折叠侧栏"
title="折叠侧栏"
aria-expanded="true"
onClick={() => setSidebarCollapsed(true)}
className="no-drag motion-press flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-surface-subtle hover:text-foreground"
><PanelLeftClose className="h-4 w-4" /></button>}
</div>}
{workspaceLayout && <button type="button" aria-label="返回首页" title="返回首页" onClick={() => navigate('/module-select')} className="classroom-brand flex w-full items-center gap-2 rounded-lg text-left">
<span className="classroom-brand-mark"><img src={classroomMark} alt="" className="h-7 w-7" /></span>

View File

@@ -15,7 +15,8 @@ import { useSettingsStore } from '@/stores/settings';
import { normalizeClassroomSidebarWidth } from '@/lib/classroom-layout';
import logoWordmarkSource from '@/assets/makelore-wordmark-source.png';
import type { SidebarPeekSource } from './sidebar-peek';
import { TITLEBAR_LOGO_WIDTH, WINDOWS_TITLEBAR_CONTROLS_WIDTH } from './titlebar-metrics';
import { ModuleSwitcher } from './ModuleSwitcher';
import { getClassroomTitlebarWidth, TITLEBAR_LOGO_WIDTH, WINDOWS_TITLEBAR_CONTROLS_WIDTH } from './titlebar-metrics';
type SidebarPeekChange = (open: boolean, source: SidebarPeekSource) => void;
@@ -69,6 +70,7 @@ function ProductTitleBar({
}) {
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
const sidebarWidth = useSettingsStore((state) => normalizeClassroomSidebarWidth(state.classroomSidebarWidth));
const classroomTitlebarWidth = getClassroomTitlebarWidth(nativeTrafficLights ? 'darwin' : 'win32', sidebarCollapsed, sidebarWidth);
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
const toolbarButtonClass = 'no-drag motion-press flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-surface-subtle hover:text-foreground';
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
@@ -94,13 +96,13 @@ function ProductTitleBar({
className={cn(
toolbarButtonClass,
'pointer-events-auto absolute top-1 z-[401]',
nativeTrafficLights ? 'left-[88px]' : 'left-2',
workspaceLayout ? 'right-2' : nativeTrafficLights ? 'left-[88px]' : 'left-2',
)}
onClick={toggleSidebar}
onPointerEnter={() => handleSidebarPeekEnter('toggle')}
onPointerLeave={() => handleSidebarPeekLeave('toggle')}
onFocus={() => handleSidebarPeekEnter('focus')}
onBlur={() => handleSidebarPeekLeave('focus')}
onFocus={!workspaceLayout ? () => handleSidebarPeekEnter('focus') : undefined}
onBlur={!workspaceLayout ? () => handleSidebarPeekLeave('focus') : undefined}
>
{sidebarCollapsed ? <PanelLeft className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
</button>
@@ -110,10 +112,23 @@ function ProductTitleBar({
data-testid="titlebar-sidebar-toggle-layer"
className={cn(
'no-drag pointer-events-auto fixed left-0 top-0 z-[400] h-10',
nativeTrafficLights ? 'w-[132px]' : 'w-[52px]',
!workspaceLayout && (nativeTrafficLights ? 'w-[132px]' : 'w-[52px]'),
)}
style={workspaceLayout ? { width: classroomTitlebarWidth } : undefined}
onPointerEnter={workspaceLayout ? () => handleSidebarPeekEnter('titlebar') : undefined}
onPointerLeave={workspaceLayout ? () => handleSidebarPeekLeave('titlebar') : undefined}
onFocusCapture={workspaceLayout ? () => handleSidebarPeekEnter('focus') : undefined}
onBlurCapture={workspaceLayout ? (event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) handleSidebarPeekLeave('focus');
} : undefined}
>
{sidebarToggle}
{workspaceLayout ? <div
data-testid="classroom-titlebar-navigation"
className={cn('flex h-full items-center gap-2 pr-12', nativeTrafficLights ? 'pl-[88px]' : 'pl-2')}
>
<ModuleSwitcher sidebarCollapsed={false} toolbar iconOnly />
{sidebarToggle}
</div> : sidebarToggle}
</div>
);
const titlebarLogo = (
@@ -155,16 +170,16 @@ function ProductTitleBar({
<>
<div
data-testid="titlebar-sidebar-surface"
style={workspaceLayout && !sidebarTitlebarHidden ? { width: sidebarWidth, minWidth: sidebarWidth, flexBasis: sidebarWidth } : undefined}
style={workspaceLayout ? { width: classroomTitlebarWidth, minWidth: classroomTitlebarWidth, flexBasis: classroomTitlebarWidth } : undefined}
className={cn(
'flex h-full shrink-0 items-center gap-1 px-2',
sidebarTitlebarHidden
workspaceLayout
? ''
: sidebarTitlebarHidden
? nativeTrafficLights
? 'w-[132px]'
: 'w-[52px]'
: workspaceLayout
? ''
: 'w-64',
: 'w-64',
overlay
? 'pointer-events-none border-r-0 bg-transparent'
: sidebarTitlebarHidden
@@ -178,7 +193,7 @@ function ProductTitleBar({
onPointerEnter={sidebarPreviewTitlebarVisible ? () => handleSidebarPeekEnter('titlebar') : undefined}
onPointerLeave={sidebarPreviewTitlebarVisible ? () => handleSidebarPeekLeave('titlebar') : undefined}
/>
{(!workspaceLayout || sidebarCollapsed) && (typeof document !== 'undefined' ? createPortal(sidebarTogglePortal, document.body) : sidebarTogglePortal)}
{typeof document !== 'undefined' ? createPortal(sidebarTogglePortal, document.body) : sidebarTogglePortal}
</>
) : null}
{integrated ? (

View File

@@ -1,4 +1,13 @@
import { normalizeClassroomSidebarWidth } from '@/lib/classroom-layout';
export const TITLEBAR_LOGO_WIDTH = 96;
// Three 44px buttons plus the control container's 8px padding on each side.
export const WINDOWS_TITLEBAR_CONTROLS_WIDTH = 148;
/** Keep the Code navigation and conversation header outside each other's hit areas. */
export function getClassroomTitlebarWidth(platform: string | undefined, sidebarCollapsed: boolean, sidebarWidth: number): number {
// Two 32px controls, an 8px gap/right inset, and the native traffic-light safe area.
const minimum = platform === 'darwin' ? 168 : 88;
return sidebarCollapsed ? minimum : Math.max(minimum, normalizeClassroomSidebarWidth(sidebarWidth));
}

View File

@@ -14,7 +14,7 @@ import { CodingConversationRenameDialog } from './CodingConversationRenameDialog
import { cn } from '@/lib/utils';
import { useSettingsStore } from '@/stores/settings';
import { normalizeClassroomSidebarWidth } from '@/lib/classroom-layout';
import { WINDOWS_TITLEBAR_CONTROLS_WIDTH } from '@/components/layout/titlebar-metrics';
import { getClassroomTitlebarWidth, WINDOWS_TITLEBAR_CONTROLS_WIDTH } from '@/components/layout/titlebar-metrics';
import type { ConversationSnapshot } from '@/types/coding-conversation';
import type { CodingConversationMetadata } from '@/types/coding-project';
@@ -189,7 +189,7 @@ export function CodingConversationHeader({
className="pointer-events-none fixed right-0 top-0 z-[80] h-10"
data-testid="coding-conversation-header-titlebar"
style={{
left: `${sidebarCollapsed ? (platform === 'darwin' ? 132 : 52) : Math.max(sidebarWidth, platform === 'darwin' ? 132 : 0)}px`,
left: `${getClassroomTitlebarWidth(platform, sidebarCollapsed, sidebarWidth)}px`,
// Keep both actions and their hit area outside the product chrome.
right: platform === 'win32' ? WINDOWS_TITLEBAR_CONTROLS_WIDTH : 0,
}}

View File

@@ -6,10 +6,10 @@
[data-testid="main-layout"][data-classroom="true"] [data-testid="titlebar-sidebar-surface"] { background: #f3f7fb; border-color: #dde5ee; box-shadow: none; backdrop-filter: none; }
[data-testid="sidebar"][data-classroom="true"] { background: #f3f7fb; border-color: #dde5ee; box-shadow: none; backdrop-filter: none; color: #26384d; }
[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-header { padding-top: 0; padding-bottom: 0; }
[data-testid="sidebar"][data-classroom="true"] [data-testid="classroom-sidebar-navigation"] { align-items: flex-start; }
[data-testid="sidebar"][data-classroom="true"] .classroom-brand { gap: 10px; padding: 0 2px 8px; }
[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-header .community-link[data-variant="sidebar"] { min-height: 36px; margin-top: 10px; gap: 4px; padding: 6px 4px; border-color: #cfddc3; border-radius: 11px; background: #e8f0df; }
[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-header .community-link[data-variant="sidebar"]:hover:not(:disabled) { background: #dfeacf; border-color: #b7c6a4; }
[data-testid="sidebar"][data-classroom="true"] [data-testid="classroom-sidebar-navigation"] { align-items: center; }
[data-testid="sidebar"][data-classroom="true"] .classroom-brand { height: 46px; gap: 10px; padding: 0 2px; }
[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-header .community-link[data-variant="sidebar"] { min-height: 41px; margin-top: 10px; gap: 9px; padding: 8px 11px; border: 1px solid #d5e1ed; border-radius: 11px; background: #f8faf5; box-shadow: 0 3px 0 #e3eaf3; font-size: 14px; font-weight: 500; line-height: 20px; white-space: normal; }
[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-header .community-link[data-variant="sidebar"]:hover:not(:disabled) { background: #eef3e7; border-color: #b7c6a4; }
[data-testid="sidebar"][data-classroom="true"] .classroom-brand:hover { background: #eaf1f8; }
.classroom-brand-mark { display: grid; place-items: center; width: 40px; height: 42px; flex-shrink: 0; border: 1px solid #f1d1aa; border-radius: 12px; background: #fff0dd; box-shadow: 0 3px 0 #ecdbc5; }
.classroom-brand-caption { display: block; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: #64758a; }