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,181 +341,156 @@ 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) ? (
|
||||
<div className={SETTINGS_OPTION_STACK_CLASS}>
|
||||
{launchAtLoginSupported ? (
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="sessions.desktop-launch-at-login"
|
||||
checked={launchAtLoginEnabled}
|
||||
onChange={(checked) => {
|
||||
if (checked === launchAtLoginEnabled) return;
|
||||
void handleLaunchAtLoginToggle();
|
||||
}}
|
||||
disabled={isSavingLaunchAtLogin}
|
||||
label={t('settings.openchamber.desktopNetwork.field.launchAtLogin')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.launchAtLoginDescription')}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.launchAtLoginAria')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isMacDesktop ? (
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="sessions.desktop-mac-menu-bar"
|
||||
checked={draftMacMenuBarEnabled}
|
||||
onChange={setDraftMacMenuBarEnabled}
|
||||
disabled={isLoading || isSaving}
|
||||
label={t('settings.openchamber.desktopNetwork.field.macMenuBar')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.macMenuBarDescription')}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.macMenuBarAria')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{minimizeToTraySupported ? (
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="sessions.desktop-minimize-to-tray"
|
||||
checked={minimizeToTrayEnabled}
|
||||
onChange={(checked) => {
|
||||
if (checked === minimizeToTrayEnabled) return;
|
||||
void handleMinimizeToTrayToggle();
|
||||
}}
|
||||
disabled={isSavingMinimizeToTray}
|
||||
label={t('settings.openchamber.desktopNetwork.field.minimizeToTray')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.minimizeToTrayDescription')}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.minimizeToTrayAria')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{keepAwakeSupported ? (
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="sessions.desktop-keep-awake"
|
||||
checked={keepAwakeEnabled}
|
||||
onChange={(checked) => {
|
||||
if (checked === keepAwakeEnabled) return;
|
||||
void handleKeepAwakeToggle();
|
||||
}}
|
||||
disabled={isSavingKeepAwake}
|
||||
label={t('settings.openchamber.desktopNetwork.field.keepAwake')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.keepAwakeDescription')}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.keepAwakeAria')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<SettingsStackedField
|
||||
settingsItem="sessions.desktop-ui-password"
|
||||
label={(
|
||||
<label htmlFor="desktop-ui-password">
|
||||
{t('settings.openchamber.desktopPassword.field.password')}
|
||||
</label>
|
||||
)}
|
||||
info={t('settings.openchamber.desktopPassword.field.passwordDescription')}
|
||||
>
|
||||
<Input
|
||||
id="desktop-ui-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className="h-8 min-w-0 flex-1"
|
||||
value={draftPassword}
|
||||
onChange={(event) => handlePasswordChange(event.target.value)}
|
||||
placeholder={t('settings.openchamber.desktopPassword.field.passwordPlaceholder')}
|
||||
disabled={isLoading || isSaving}
|
||||
required={draftValue}
|
||||
aria-invalid={lanRequiresPassword}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => setShowPassword((current: boolean) => !current)}
|
||||
className={SETTINGS_ICON_BUTTON_CLASS}
|
||||
aria-label={t(showPassword ? 'settings.openchamber.desktopPassword.actions.hidePassword' : 'settings.openchamber.desktopPassword.actions.showPassword')}
|
||||
aria-pressed={showPassword}
|
||||
>
|
||||
<Icon name={showPassword ? 'eye-off' : 'eye'} className="h-4 w-4" />
|
||||
</Button>
|
||||
</SettingsStackedField>
|
||||
|
||||
<div className={SETTINGS_OPTION_STACK_CLASS}>
|
||||
<SettingsSection title={t('settings.openchamber.desktopNetwork.title')}>
|
||||
<div className="space-y-3">
|
||||
{(launchAtLoginSupported || isMacDesktop || minimizeToTraySupported || keepAwakeSupported) ? (
|
||||
<div className={SETTINGS_OPTION_STACK_CLASS}>
|
||||
{launchAtLoginSupported ? (
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="sessions.desktop-lan-access"
|
||||
checked={draftValue}
|
||||
onChange={setDraftValue}
|
||||
disabled={isLoading || isSaving}
|
||||
label={t('settings.openchamber.desktopNetwork.field.allowLanAccess')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.allowLanAccessDescription')}
|
||||
description={(
|
||||
<>
|
||||
<span className="block text-[var(--status-warning)]/85">
|
||||
{t('settings.openchamber.desktopNetwork.field.warning')}
|
||||
</span>
|
||||
{lanRequiresPassword || lanBlockedByMissingPassword ? (
|
||||
<span className="block text-[var(--status-warning)]/85">
|
||||
{t('settings.openchamber.desktopNetwork.field.passwordRequiredWarning')}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.allowLanAccessAria')}
|
||||
settingsItem="sessions.desktop-launch-at-login"
|
||||
checked={launchAtLoginEnabled}
|
||||
onChange={(checked) => {
|
||||
if (checked === launchAtLoginEnabled) return;
|
||||
void handleLaunchAtLoginToggle();
|
||||
}}
|
||||
disabled={isSavingLaunchAtLogin}
|
||||
label={t('settings.openchamber.desktopNetwork.field.launchAtLogin')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.launchAtLoginDescription')}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.launchAtLoginAria')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="typography-micro text-[var(--status-error)]">{error}</div>
|
||||
) : null}
|
||||
|
||||
{lanUrl ? (
|
||||
<div className="typography-micro text-muted-foreground/80">
|
||||
{isDirty && !savedValue
|
||||
? t('settings.openchamber.desktopNetwork.hint.openAfterRestart')
|
||||
: t('settings.openchamber.desktopNetwork.hint.openNow')}
|
||||
<span className="font-mono text-foreground">{lanUrl}</span>
|
||||
</div>
|
||||
{isMacDesktop ? (
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="sessions.desktop-mac-menu-bar"
|
||||
checked={draftMacMenuBarEnabled}
|
||||
onChange={setDraftMacMenuBarEnabled}
|
||||
disabled={isLoading || isSaving}
|
||||
label={t('settings.openchamber.desktopNetwork.field.macMenuBar')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.macMenuBarDescription')}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.macMenuBarAria')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="flex justify-start py-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
onClick={handleSaveAndRestart}
|
||||
disabled={saveDisabled}
|
||||
className="shrink-0 !font-normal"
|
||||
>
|
||||
{isSaving ? t('settings.common.actions.saving') : t('settings.openchamber.desktopNetwork.actions.saveAndRestart')}
|
||||
</Button>
|
||||
</div>
|
||||
{minimizeToTraySupported ? (
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="sessions.desktop-minimize-to-tray"
|
||||
checked={minimizeToTrayEnabled}
|
||||
onChange={(checked) => {
|
||||
if (checked === minimizeToTrayEnabled) return;
|
||||
void handleMinimizeToTrayToggle();
|
||||
}}
|
||||
disabled={isSavingMinimizeToTray}
|
||||
label={t('settings.openchamber.desktopNetwork.field.minimizeToTray')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.minimizeToTrayDescription')}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.minimizeToTrayAria')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{keepAwakeSupported ? (
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="sessions.desktop-keep-awake"
|
||||
checked={keepAwakeEnabled}
|
||||
onChange={(checked) => {
|
||||
if (checked === keepAwakeEnabled) return;
|
||||
void handleKeepAwakeToggle();
|
||||
}}
|
||||
disabled={isSavingKeepAwake}
|
||||
label={t('settings.openchamber.desktopNetwork.field.keepAwake')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.keepAwakeDescription')}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.keepAwakeAria')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</SettingsSection>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<SettingsStackedField
|
||||
settingsItem="sessions.desktop-ui-password"
|
||||
label={(
|
||||
<label htmlFor="desktop-ui-password">
|
||||
{t('settings.openchamber.desktopPassword.field.password')}
|
||||
</label>
|
||||
)}
|
||||
info={t('settings.openchamber.desktopPassword.field.passwordDescription')}
|
||||
>
|
||||
<Input
|
||||
id="desktop-ui-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className="h-8 min-w-0 flex-1"
|
||||
value={draftPassword}
|
||||
onChange={(event) => handlePasswordChange(event.target.value)}
|
||||
placeholder={t('settings.openchamber.desktopPassword.field.passwordPlaceholder')}
|
||||
disabled={isLoading || isSaving}
|
||||
required={draftValue}
|
||||
aria-invalid={lanRequiresPassword}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => setShowPassword((current: boolean) => !current)}
|
||||
className={SETTINGS_ICON_BUTTON_CLASS}
|
||||
aria-label={t(showPassword ? 'settings.openchamber.desktopPassword.actions.hidePassword' : 'settings.openchamber.desktopPassword.actions.showPassword')}
|
||||
aria-pressed={showPassword}
|
||||
>
|
||||
<Icon name={showPassword ? 'eye-off' : 'eye'} className="h-4 w-4" />
|
||||
</Button>
|
||||
</SettingsStackedField>
|
||||
|
||||
<div className={SETTINGS_OPTION_STACK_CLASS}>
|
||||
<SettingsCheckboxRow
|
||||
settingsItem="sessions.desktop-lan-access"
|
||||
checked={draftValue}
|
||||
onChange={setDraftValue}
|
||||
disabled={isLoading || isSaving}
|
||||
label={t('settings.openchamber.desktopNetwork.field.allowLanAccess')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.allowLanAccessDescription')}
|
||||
description={(
|
||||
<>
|
||||
<span className="block text-[var(--status-warning)]/85">
|
||||
{t('settings.openchamber.desktopNetwork.field.warning')}
|
||||
</span>
|
||||
{lanRequiresPassword || lanBlockedByMissingPassword ? (
|
||||
<span className="block text-[var(--status-warning)]/85">
|
||||
{t('settings.openchamber.desktopNetwork.field.passwordRequiredWarning')}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
ariaLabel={t('settings.openchamber.desktopNetwork.field.allowLanAccessAria')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="typography-micro text-[var(--status-error)]">{error}</div>
|
||||
) : null}
|
||||
|
||||
{lanUrl ? (
|
||||
<div className="typography-micro text-muted-foreground/80">
|
||||
{isDirty && !savedValue
|
||||
? t('settings.openchamber.desktopNetwork.hint.openAfterRestart')
|
||||
: t('settings.openchamber.desktopNetwork.hint.openNow')}
|
||||
<span className="font-mono text-foreground">{lanUrl}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex justify-start py-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
onClick={handleSaveAndRestart}
|
||||
disabled={saveDisabled}
|
||||
className="shrink-0 !font-normal"
|
||||
>
|
||||
{isSaving ? t('settings.common.actions.saving') : t('settings.openchamber.desktopNetwork.actions.saveAndRestart')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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