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>
This commit is contained in:
@@ -48,10 +48,19 @@ export const WindowsWindowControls = React.memo(function WindowsWindowControls({
|
||||
return null;
|
||||
}
|
||||
|
||||
const buttonClassName = 'app-region-no-drag inline-flex h-12 w-11 items-center justify-center text-muted-foreground transition-colors hover:bg-interactive-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary';
|
||||
const containerClassName = position === 'left'
|
||||
? 'app-region-no-drag -ml-3 mr-2 flex h-12 shrink-0 items-center'
|
||||
: 'app-region-no-drag -mr-3 ml-2 flex h-12 shrink-0 items-center';
|
||||
// Left-side controls sit in the same cluster as h-8 titlebar icon buttons
|
||||
// (app menu / sidebar / project actions). Match that size and avoid negative
|
||||
// margins so TitlebarLeftControls can publish an accurate reserved width —
|
||||
// otherwise the project-actions chevron overlaps the session title.
|
||||
// Right-side controls keep a taller Windows-style hit target.
|
||||
const isLeft = position === 'left';
|
||||
const buttonClassName = cn(
|
||||
'app-region-no-drag inline-flex items-center justify-center text-muted-foreground transition-colors hover:bg-interactive-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
isLeft ? 'h-8 w-8 rounded-md' : 'h-12 w-11',
|
||||
);
|
||||
const containerClassName = isLeft
|
||||
? 'app-region-no-drag mr-1 flex h-8 shrink-0 items-center'
|
||||
: 'app-region-no-drag ml-1 flex h-12 shrink-0 items-center';
|
||||
|
||||
return (
|
||||
<div className={containerClassName} aria-label={t('header.windowControls.groupAria')}>
|
||||
|
||||
@@ -56,7 +56,9 @@ export const TitlebarLeftControls: React.FC = () => {
|
||||
}
|
||||
|
||||
const publishWidth = () => {
|
||||
const width = node.getBoundingClientRect().width;
|
||||
// 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`);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,40 +14,24 @@ import {
|
||||
setDesktopKeepAwake,
|
||||
setDesktopLaunchAtLogin,
|
||||
setDesktopMinimizeToTray,
|
||||
usesFramelessElectronChrome,
|
||||
type DesktopWindowControlsPosition,
|
||||
} from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingsCheckboxRow,
|
||||
SettingsChipGroup,
|
||||
SettingsFieldRow,
|
||||
SETTINGS_OPTION_STACK_CLASS,
|
||||
SettingsStackedField,
|
||||
SETTINGS_ICON_BUTTON_CLASS,
|
||||
} from '@/components/sections/shared/SettingsSection';
|
||||
|
||||
const WINDOW_CONTROLS_POSITION_OPTIONS: Array<{ id: DesktopWindowControlsPosition; labelKey: string }> = [
|
||||
{ id: 'auto', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsAuto' },
|
||||
{ id: 'left', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsLeft' },
|
||||
{ id: 'right', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsRight' },
|
||||
];
|
||||
|
||||
export const DesktopNetworkSettings: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const tUnsafe = React.useCallback((key: string) => t(key as Parameters<typeof t>[0]), [t]);
|
||||
const isLocalDesktop = isDesktopShell() && isDesktopLocalOriginActive();
|
||||
const isMacDesktop = isLocalDesktop
|
||||
&& typeof window !== 'undefined'
|
||||
&& window.__OPENCHAMBER_PLATFORM__ === 'darwin';
|
||||
const showWindowControlsPosition = usesFramelessElectronChrome();
|
||||
const desktopWindowControlsPosition = useUIStore((state) => state.desktopWindowControlsPosition);
|
||||
const setDesktopWindowControlsPosition = useUIStore((state) => state.setDesktopWindowControlsPosition);
|
||||
const [savedValue, setSavedValue] = React.useState(false);
|
||||
const [draftValue, setDraftValue] = React.useState(false);
|
||||
const [savedPassword, setSavedPassword] = React.useState('');
|
||||
@@ -242,11 +226,6 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleWindowControlsPositionChange = React.useCallback((value: DesktopWindowControlsPosition) => {
|
||||
setDesktopWindowControlsPosition(value);
|
||||
void updateDesktopSettings({ desktopWindowControlsPosition: value });
|
||||
}, [setDesktopWindowControlsPosition]);
|
||||
|
||||
const handleLaunchAtLoginToggle = React.useCallback(async () => {
|
||||
if (!launchAtLoginSupported || isSavingLaunchAtLogin) {
|
||||
return;
|
||||
@@ -362,34 +341,11 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
}
|
||||
}, [draftMacMenuBarEnabled, draftPassword, draftValue, isDirty, t]);
|
||||
|
||||
if (!isLocalDesktop && !showWindowControlsPosition) {
|
||||
if (!isLocalDesktop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showWindowControlsPosition ? (
|
||||
<SettingsSection title={t('settings.openchamber.desktopNetwork.field.windowControlsPosition')}>
|
||||
<SettingsFieldRow
|
||||
settingsItem="sessions.desktop-window-controls-position"
|
||||
label={t('settings.openchamber.desktopNetwork.field.windowControlsPositionDescription')}
|
||||
alignEnd={false}
|
||||
controlClassName="flex-col items-stretch"
|
||||
>
|
||||
<SettingsChipGroup
|
||||
value={desktopWindowControlsPosition}
|
||||
options={WINDOW_CONTROLS_POSITION_OPTIONS.map((option) => ({
|
||||
value: option.id,
|
||||
label: tUnsafe(option.labelKey),
|
||||
}))}
|
||||
onChange={handleWindowControlsPositionChange}
|
||||
aria-label={t('settings.openchamber.desktopNetwork.field.windowControlsPositionAria')}
|
||||
/>
|
||||
</SettingsFieldRow>
|
||||
</SettingsSection>
|
||||
) : null}
|
||||
|
||||
{isLocalDesktop ? (
|
||||
<SettingsSection title={t('settings.openchamber.desktopNetwork.title')}>
|
||||
<div className="space-y-3">
|
||||
{(launchAtLoginSupported || isMacDesktop || minimizeToTraySupported || keepAwakeSupported) ? (
|
||||
@@ -536,7 +492,5 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import { DesktopNetworkSettings } from './DesktopNetworkSettings';
|
||||
import { KeyboardShortcutsSettings } from './KeyboardShortcutsSettings';
|
||||
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime, usesFramelessElectronChrome } from '@/lib/desktop';
|
||||
import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { isCapacitorApp } from '@/lib/platform';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch';
|
||||
@@ -42,7 +42,7 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
||||
const showAbout = isMobile && isWebRuntime();
|
||||
const isVSCode = isVSCodeRuntime();
|
||||
void runtimeEndpointEpoch;
|
||||
const showDesktopNetworkSettings = isDesktopShell() && (isDesktopLocalOriginActive() || usesFramelessElectronChrome());
|
||||
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
|
||||
|
||||
// If no section specified, show all (mobile/legacy behavior)
|
||||
if (!section) {
|
||||
@@ -135,7 +135,7 @@ const GeneralSectionContent: React.FC = () => {
|
||||
const isVSCode = isVSCodeRuntime();
|
||||
const runtimeEndpointEpoch = useRuntimeEndpointEpoch();
|
||||
void runtimeEndpointEpoch;
|
||||
const showDesktopNetworkSettings = isDesktopShell() && (isDesktopLocalOriginActive() || usesFramelessElectronChrome());
|
||||
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
|
||||
// Passkeys only work against the browser's WebAuthn UI on the web surface —
|
||||
// desktop shell, VS Code, and the Capacitor app never show the login screen.
|
||||
const showPasskeySettings = isWebRuntime() && !isDesktopShell() && !isVSCode && !isCapacitorApp();
|
||||
@@ -162,6 +162,7 @@ const VisualSectionContent: React.FC = () => {
|
||||
const isVSCode = isVSCodeRuntime();
|
||||
return <OpenChamberVisualSettings visibleSettings={[
|
||||
'theme',
|
||||
'windowControlsPosition',
|
||||
'pwaInstallName',
|
||||
'pwaOrientation',
|
||||
'mobileKeyboardMode',
|
||||
|
||||
@@ -17,7 +17,14 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { invokeDesktop, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import {
|
||||
invokeDesktop,
|
||||
isDesktopShell,
|
||||
isVSCodeRuntime,
|
||||
isWebRuntime,
|
||||
usesFramelessElectronChrome,
|
||||
type DesktopWindowControlsPosition,
|
||||
} from '@/lib/desktop';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { usePwaDetection } from '@/hooks/usePwaDetection';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
@@ -271,7 +278,13 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
|
||||
return mode === 'markdown' ? 'markdown' : 'plain';
|
||||
};
|
||||
|
||||
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'terminalShell' | 'terminalLoginShell' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'promptNavigatorEnabled' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
|
||||
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'windowControlsPosition' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'terminalShell' | 'terminalLoginShell' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'promptNavigatorEnabled' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
|
||||
|
||||
const WINDOW_CONTROLS_POSITION_OPTIONS: Array<{ id: DesktopWindowControlsPosition; labelKey: string }> = [
|
||||
{ id: 'auto', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsAuto' },
|
||||
{ id: 'left', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsLeft' },
|
||||
{ id: 'right', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsRight' },
|
||||
];
|
||||
|
||||
interface OpenChamberVisualSettingsProps {
|
||||
/** Which settings to show. If undefined, shows all. */
|
||||
@@ -408,6 +421,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
);
|
||||
const dockBadgeEnabled = useUIStore(state => state.dockBadgeEnabled);
|
||||
const setDockBadgeEnabled = useUIStore(state => state.setDockBadgeEnabled);
|
||||
const showWindowControlsPosition = usesFramelessElectronChrome();
|
||||
const desktopWindowControlsPosition = useUIStore((state) => state.desktopWindowControlsPosition);
|
||||
const setDesktopWindowControlsPosition = useUIStore((state) => state.setDesktopWindowControlsPosition);
|
||||
const [chatRenderPreviewTick, setChatRenderPreviewTick] = React.useState(0);
|
||||
const reportUsage = useUIStore(state => state.reportUsage);
|
||||
const setReportUsage = useUIStore(state => state.setReportUsage);
|
||||
@@ -418,6 +434,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
void updateDesktopSettings({ reportUsage: enabled });
|
||||
}, [setReportUsage]);
|
||||
|
||||
const handleWindowControlsPositionChange = React.useCallback((value: DesktopWindowControlsPosition) => {
|
||||
setDesktopWindowControlsPosition(value);
|
||||
void updateDesktopSettings({ desktopWindowControlsPosition: value });
|
||||
}, [setDesktopWindowControlsPosition]);
|
||||
|
||||
const shouldAnimateChatPreview = (isSettingsDialogOpen || isMobile || isVSCodeRuntime())
|
||||
&& (visibleSettings ? visibleSettings.includes('chatRenderMode') : true);
|
||||
|
||||
@@ -596,11 +617,12 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
const isVSCode = isVSCodeRuntime();
|
||||
const hasThemeSettings = shouldShow('theme') && !isVSCode;
|
||||
const showWindowControlsPositionSetting = shouldShow('windowControlsPosition') && showWindowControlsPosition;
|
||||
const hasLocalizationSettings = shouldShow('theme') || shouldShow('timeFormat') || shouldShow('weekStart');
|
||||
const showMobileLayoutSetting = isMobile && isWebRuntime() && !isDesktopShell() && !isVSCode;
|
||||
const hasAppearanceSettings = isVSCode
|
||||
? hasLocalizationSettings
|
||||
: (shouldShow('theme') || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
|
||||
: (shouldShow('theme') || showWindowControlsPositionSetting || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
|
||||
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('editorFontSize') || shouldShow('spacing') || (shouldShow('inputBarOffset') && isMobile);
|
||||
const hasNavigationSettings = (shouldShow('terminalQuickKeys') && !isMobile) || ((shouldShow('terminalShell') || shouldShow('terminalLoginShell')) && !isVSCode) || shouldShow('fileEditorKeymap') || (shouldShow('expandedEditorToolbar') && !isVSCode);
|
||||
const hasBehaviorSettings = shouldShow('mermaidRendering')
|
||||
@@ -980,6 +1002,25 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{showWindowControlsPositionSetting && (
|
||||
<SettingsSection
|
||||
title={t('settings.openchamber.desktopNetwork.field.windowControlsPosition')}
|
||||
description={t('settings.openchamber.desktopNetwork.field.windowControlsPositionDescription')}
|
||||
divider={hasThemeSettings}
|
||||
settingsItem="sessions.desktop-window-controls-position"
|
||||
>
|
||||
<SettingsChipGroup
|
||||
value={desktopWindowControlsPosition}
|
||||
options={WINDOW_CONTROLS_POSITION_OPTIONS.map((option) => ({
|
||||
value: option.id,
|
||||
label: tUnsafe(option.labelKey),
|
||||
}))}
|
||||
onChange={handleWindowControlsPositionChange}
|
||||
aria-label={t('settings.openchamber.desktopNetwork.field.windowControlsPositionAria')}
|
||||
/>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{hasLocalizationSettings && (
|
||||
<SettingsSection title={t('settings.openchamber.visual.section.localization')}>
|
||||
<SettingsTwoColumn>
|
||||
|
||||
@@ -246,8 +246,8 @@ export const getElectronPlatform = (): string | null => {
|
||||
return typeof platform === 'string' ? platform : null;
|
||||
};
|
||||
|
||||
/** Width of the three in-app window control buttons (3 × w-11). */
|
||||
export const DESKTOP_WINDOW_CONTROLS_WIDTH_PX = 132;
|
||||
/** Width of the three in-app window control buttons when placed on the left (3 × w-8). */
|
||||
export const DESKTOP_WINDOW_CONTROLS_WIDTH_PX = 96;
|
||||
|
||||
/** Windows and Linux use frameless windows with in-app minimize/maximize/close controls. */
|
||||
export const usesFramelessElectronChrome = (): boolean => {
|
||||
|
||||
@@ -392,7 +392,7 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'sessions.desktop-window-controls-position',
|
||||
page: 'general',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.desktopNetwork.field.windowControlsPosition',
|
||||
descriptionKey: 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription',
|
||||
keywords: ['desktop', 'window', 'controls', 'minimize', 'maximize', 'close', 'titlebar', 'linux', 'windows'],
|
||||
|
||||
Reference in New Issue
Block a user