feat(desktop): gate traffic-lights behind window-controls style setting

Add a Window controls Style setting ('classic' | 'traffic-lights',
default 'classic') next to the existing Position setting. All four
position×style combinations render correctly with side-driven order
(close,min,max left; min,max,close right).

- Restore deleted left-classic branch (h-8 w-8 rounded-md buttons)
- Add right-traffic-lights branch (cluster with ml-1 container)
- Branch on style instead of side in WindowsWindowControls
- Store field + setter with 'classic' default (no migration needed;
  Zustand persist shallow-merges over initial state)
- Settings UI: SettingsTwoColumn with Position chips + Style dropdown
- Sanitize on client (persistence.ts) and server (settings-helpers.js)
- 10 locale dictionaries updated (add-only, no existing keys changed)
- Traffic-light button spacing 8px→10px edge-to-edge
- Delete dead DESKTOP_WINDOW_CONTROLS_WIDTH_PX constant
This commit is contained in:
Pablo Gonzalez
2026-07-29 19:38:48 +02:00
parent 029f1705db
commit 9001e38c54
19 changed files with 80 additions and 162 deletions
@@ -16,24 +16,24 @@ const TRAFFIC_LIGHT_COLORS: Record<DesktopWindowControlAction, { fill: string; g
maximize: { fill: '#28C940', glyph: '#006200' },
};
const TrafficLightGlyph: React.FC<{ action: DesktopWindowControlAction }> = ({ action }) => {
const TrafficLightGlyph: React.FC<{ action: DesktopWindowControlAction; isMaximized: boolean }> = ({ action, isMaximized }) => {
if (action === 'close') {
return (
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth={1.2} strokeLinecap="round" className="size-[9px]" aria-hidden>
<path d="M3.5 3.5 L8.5 8.5 M8.5 3.5 L3.5 8.5" />
</svg>
);
return <Icon name="close" className="size-[10px]" />;
}
if (action === 'minimize') {
return (
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth={1.4} strokeLinecap="round" className="size-[9px]" aria-hidden>
<path d="M3 6 L9 6" />
</svg>
);
return <Icon name="subtract" className="size-[10px]" />;
}
// Green expand: two paired corner triangles pointing diagonally outward.
return (
<svg viewBox="0 0 12 12" fill="currentColor" className="size-[9px]" aria-hidden>
// Green expand: two filled corner triangles pointing outward (maximize)
// or inward (restore). No Remixicon equivalent exists.
// Glyphs are 10px inside the 14px circle on purpose: 9px left a half-pixel
// (2.5px) padding that rasterized lopsided at 1x DPR.
return isMaximized ? (
<svg viewBox="0 0 12 12" fill="currentColor" className="size-[10px]" aria-hidden>
<path d="M5.25 6.75 L9.75 6.75 L9.75 2.25 Z" />
<path d="M6.75 5.25 L2.25 5.25 L2.25 9.75 Z" />
</svg>
) : (
<svg viewBox="0 0 12 12" fill="currentColor" className="size-[10px]" aria-hidden>
<path d="M5.25 2.25 L9.75 2.25 L9.75 6.75 Z" />
<path d="M6.75 9.75 L2.25 9.75 L2.25 5.25 Z" />
</svg>
@@ -72,8 +72,8 @@ const TrafficLightButton: React.FC<TrafficLightButtonProps> = ({ action, isMaxim
className="flex size-3.5 items-center justify-center rounded-full shadow-[inset_0_0_0_0.5px_rgba(0,0,0,0.28)] transition-[filter] duration-75 active:brightness-90"
style={{ backgroundColor: fill, color: glyph }}
>
<span className="flex opacity-0 transition-opacity duration-75 group-hover/wctl:opacity-100">
<TrafficLightGlyph action={action} />
<span className="flex items-center justify-center opacity-0 transition-opacity duration-75 group-hover/wctl:opacity-100">
<TrafficLightGlyph action={action} isMaximized={isMaximized} />
</span>
</span>
</button>
@@ -18,7 +18,6 @@ export const iconSpriteData = {
"arrow-go-back": `<path d="M5.82843 6.99955L8.36396 9.53509L6.94975 10.9493L2 5.99955L6.94975 1.0498L8.36396 2.46402L5.82843 4.99955H13C17.4183 4.99955 21 8.58127 21 12.9996C21 17.4178 17.4183 20.9996 13 20.9996H4V18.9996H13C16.3137 18.9996 19 16.3133 19 12.9996C19 9.68584 16.3137 6.99955 13 6.99955H5.82843Z" fill="currentColor"/>`,
"arrow-go-forward": `<path d="M18.1716 6.99955H11C7.68629 6.99955 5 9.68584 5 12.9996C5 16.3133 7.68629 18.9996 11 18.9996H20V20.9996H11C6.58172 20.9996 3 17.4178 3 12.9996C3 8.58127 6.58172 4.99955 11 4.99955H18.1716L15.636 2.46402L17.0503 1.0498L22 5.99955L17.0503 10.9493L15.636 9.53509L18.1716 6.99955Z" fill="currentColor"/>`,
"arrow-left": `<path d="M7.82843 10.9999H20V12.9999H7.82843L13.1924 18.3638L11.7782 19.778L4 11.9999L11.7782 4.22168L13.1924 5.63589L7.82843 10.9999Z" fill="currentColor"/>`,
"arrow-left-long": `<path d="M22.0003 13.0001L22.0004 11.0002L5.82845 11.0002L9.77817 7.05044L8.36396 5.63623L2 12.0002L8.36396 18.3642L9.77817 16.9499L5.8284 13.0002L22.0003 13.0001Z" fill="currentColor"/>`,
"arrow-left-right": `<path d="M16.0503 12.0498L21 16.9996L16.0503 21.9493L14.636 20.5351L17.172 17.9988L4 17.9996V15.9996L17.172 15.9988L14.636 13.464L16.0503 12.0498ZM7.94975 2.0498L9.36396 3.46402L6.828 5.9988L20 5.99955V7.99955L6.828 7.9988L9.36396 10.5351L7.94975 11.9493L3 6.99955L7.94975 2.0498Z" fill="currentColor"/>`,
"arrow-left-s": `<path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z" fill="currentColor"/>`,
"arrow-right": `<path d="M16.1716 10.9999L10.8076 5.63589L12.2218 4.22168L20 11.9999L12.2218 19.778L10.8076 18.3638L16.1716 12.9999H4V10.9999H16.1716Z" fill="currentColor"/>`,
@@ -211,7 +210,6 @@ export const iconSpriteData = {
"shuffle": `<path d="M18 17.8832V16L23 19L18 22V19.9095C14.9224 19.4698 12.2513 17.4584 11.0029 14.5453L11 14.5386L10.9971 14.5453C9.57893 17.8544 6.32508 20 2.72483 20H2V18H2.72483C5.52503 18 8.05579 16.3312 9.15885 13.7574L9.91203 12L9.15885 10.2426C8.05579 7.66878 5.52503 6 2.72483 6H2V4H2.72483C6.32508 4 9.57893 6.14557 10.9971 9.45473L11 9.46141L11.0029 9.45473C12.2513 6.5416 14.9224 4.53022 18 4.09051V2L23 5L18 8V6.11684C15.7266 6.53763 13.7737 8.0667 12.8412 10.2426L12.088 12L12.8412 13.7574C13.7737 15.9333 15.7266 17.4624 18 17.8832Z" fill="currentColor"/>`,
"slash-commands-2": `<path d="M5 2C3.34315 2 2 3.34315 2 5V19C2 20.6569 3.34315 22 5 22H19C20.6569 22 22 20.6569 22 19V5C22 3.34315 20.6569 2 19 2H5ZM4 5C4 4.44772 4.44772 4 5 4H19C19.5523 4 20 4.44772 20 5V19C20 19.5523 19.5523 20 19 20H5C4.44772 20 4 19.5523 4 19V5ZM9.72318 18L16.5803 6H14.2768L7.41968 18H9.72318Z" fill="currentColor"/>`,
"smartphone": `<path d="M7 4V20H17V4H7ZM6 2H18C18.5523 2 19 2.44772 19 3V21C19 21.5523 18.5523 22 18 22H6C5.44772 22 5 21.5523 5 21V3C5 2.44772 5.44772 2 6 2ZM12 17C12.5523 17 13 17.4477 13 18C13 18.5523 12.5523 19 12 19C11.4477 19 11 18.5523 11 18C11 17.4477 11.4477 17 12 17Z" fill="currentColor"/>`,
"sort-desc": `<path d="M20 4V16H23L19 21L15 16H18V4H20ZM12 18V20H3V18H12ZM14 11V13H3V11H14ZM14 4V6H3V4H14Z" fill="currentColor"/>`,
"sparkling": `<path d="M14 4.4375C15.3462 4.4375 16.4375 3.34619 16.4375 2H17.5625C17.5625 3.34619 18.6538 4.4375 20 4.4375V5.5625C18.6538 5.5625 17.5625 6.65381 17.5625 8H16.4375C16.4375 6.65381 15.3462 5.5625 14 5.5625V4.4375ZM1 11C4.31371 11 7 8.31371 7 5H9C9 8.31371 11.6863 11 15 11V13C11.6863 13 9 15.6863 9 19H7C7 15.6863 4.31371 13 1 13V11ZM4.87601 12C6.18717 12.7276 7.27243 13.8128 8 15.124 8.72757 13.8128 9.81283 12.7276 11.124 12 9.81283 11.2724 8.72757 10.1872 8 8.87601 7.27243 10.1872 6.18717 11.2724 4.87601 12ZM17.25 14C17.25 15.7949 15.7949 17.25 14 17.25V18.75C15.7949 18.75 17.25 20.2051 17.25 22H18.75C18.75 20.2051 20.2051 18.75 22 18.75V17.25C20.2051 17.25 18.75 15.7949 18.75 14H17.25Z" fill="currentColor"/>`,
"split-cells-horizontal": `<path d="M20 3C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H20ZM11 5H5V19H11V15H13V19H19V5H13V9H11V5ZM15 9L18 12L15 15V13H9V15L6 12L9 9V11H15V9Z" fill="currentColor"/>`,
"stack": `<path d="M20.0833 15.1999L21.2854 15.9212C21.5221 16.0633 21.5989 16.3704 21.4569 16.6072C21.4146 16.6776 21.3557 16.7365 21.2854 16.7787L12.5144 22.0412C12.1977 22.2313 11.8021 22.2313 11.4854 22.0412L2.71451 16.7787C2.47772 16.6366 2.40093 16.3295 2.54301 16.0927C2.58523 16.0223 2.64413 15.9634 2.71451 15.9212L3.9166 15.1999L11.9999 20.0499L20.0833 15.1999ZM20.0833 10.4999L21.2854 11.2212C21.5221 11.3633 21.5989 11.6704 21.4569 11.9072C21.4146 11.9776 21.3557 12.0365 21.2854 12.0787L11.9999 17.6499L2.71451 12.0787C2.47772 11.9366 2.40093 11.6295 2.54301 11.3927C2.58523 11.3223 2.64413 11.2634 2.71451 11.2212L3.9166 10.4999L11.9999 15.3499L20.0833 10.4999ZM12.5144 1.30864L21.2854 6.5712C21.5221 6.71327 21.5989 7.0204 21.4569 7.25719C21.4146 7.32757 21.3557 7.38647 21.2854 7.42869L11.9999 12.9999L2.71451 7.42869C2.47772 7.28662 2.40093 6.97949 2.54301 6.7427C2.58523 6.67232 2.64413 6.61343 2.71451 6.5712L11.4854 1.30864C11.8021 1.11864 12.1977 1.11864 12.5144 1.30864ZM11.9999 3.33233L5.88723 6.99995L11.9999 10.6676L18.1126 6.99995L11.9999 3.33233Z" fill="currentColor"/>`,
@@ -751,10 +751,6 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
const option = MOBILE_KEYBOARD_MODE_OPTIONS.find((item) => item.id === mobileKeyboardMode);
return option ? tUnsafe(option.labelKey) : undefined;
}, [mobileKeyboardMode, tUnsafe]);
const selectedWindowControlsStyleLabel = React.useMemo(() => {
const option = WINDOW_CONTROLS_STYLE_OPTIONS.find((item) => item.id === desktopWindowControlsStyle);
return option ? tUnsafe(option.labelKey) : undefined;
}, [desktopWindowControlsStyle, tUnsafe]);
const handleMobileLayoutPreferenceChange = React.useCallback((value: MobileLayoutPreference) => {
if (value === mobileLayoutPreference) {
@@ -1028,7 +1024,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
{showWindowControlsPositionSetting && (
<SettingsSection
title={t('settings.openchamber.desktopNetwork.field.windowControls')}
info={t('settings.openchamber.desktopNetwork.field.windowControlsDescription')}
info={t('settings.openchamber.desktopNetwork.field.windowControlsPositionDescription')}
divider={hasThemeSettings}
>
<SettingsTwoColumn>
@@ -1050,16 +1046,15 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
label={t('settings.openchamber.desktopNetwork.field.windowControlsStyle')}
settingsItem="sessions.desktop-window-controls-style"
>
<Select value={desktopWindowControlsStyle} onValueChange={(value: DesktopWindowControlsStyle) => handleWindowControlsStyleChange(value)}>
<SelectTrigger aria-label={t('settings.openchamber.desktopNetwork.field.windowControlsStyleAria')} size={SETTINGS_SELECT_SIZE} className={SETTINGS_SELECT_TRIGGER_CLASS}>
<SelectValue>{selectedWindowControlsStyleLabel}</SelectValue>
</SelectTrigger>
<SelectContent>
{WINDOW_CONTROLS_STYLE_OPTIONS.map((option) => (
<SelectItem key={option.id} value={option.id}>{tUnsafe(option.labelKey)}</SelectItem>
))}
</SelectContent>
</Select>
<SettingsChipGroup
value={desktopWindowControlsStyle}
options={WINDOW_CONTROLS_STYLE_OPTIONS.map((option) => ({
value: option.id,
label: tUnsafe(option.labelKey),
}))}
onChange={handleWindowControlsStyleChange}
aria-label={t('settings.openchamber.desktopNetwork.field.windowControlsStyleAria')}
/>
</SettingsStackedField>
</SettingsTwoColumn>
</SettingsSection>
-11
View File
@@ -71,17 +71,6 @@ html[data-oc-vibrancy] .main-content-safe-area {
background-color: transparent !important;
}
/* ── Linux frameless window border ────────────────────────────────────────
`data-oc-window-border` is set on <html> by initLinuxWindowBorder()
(ui/src/lib/desktop.ts), and `data-oc-window-maximized` drops the border
so a maximized window stays flush with screen edges. Rounded corners
were removed: Electron 41's transparent-window path on Linux is unreliable
without --disable-gpu, and native rounded corners landed upstream only in
Electron 43 (PR #51459). */
html[data-oc-window-border]:not([data-oc-window-maximized]) #root {
border: 1px solid var(--border);
}
/* Left sidebar: solid until the native layer is ready, then a translucent
overlay so the vibrancy frosts through. */
html[data-oc-vibrancy] .oc-vibrancy-surface {
-31
View File
@@ -300,37 +300,6 @@ export const hasDesktopInvoke = (): boolean => {
return typeof getDesktopBridge()?.invoke === 'function';
};
/**
* Linux frameless Electron windows show a 1px CSS border on #root (see
* index.css). This function flips the attribute on <html> so the rule
* matches, and tracks `data-oc-window-maximized` so the border drops when
* maximized (a 1px line inset from every screen edge would look off).
* No-op outside Linux frameless Electron.
*/
export const initLinuxWindowBorder = (): void => {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
if (!usesFramelessElectronChrome()) return;
const platform = getElectronPlatform();
if (platform !== 'linux') return;
const root = document.documentElement;
root.setAttribute('data-oc-window-border', '');
const applyMaximized = (maximized: boolean) => {
root.toggleAttribute('data-oc-window-maximized', maximized);
};
void invokeDesktop<{ maximized?: boolean }>('desktop_get_current_window_state')
.then((state) => applyMaximized(Boolean(state?.maximized)))
.catch(() => {});
const onMaximizedChange = (event: Event) => {
const detail = (event as CustomEvent<{ maximized?: boolean }>).detail;
applyMaximized(Boolean(detail?.maximized));
};
window.addEventListener('openchamber:window-maximized-changed', onMaximizedChange);
};
export const canUseElectronDesktopIPC = (): boolean => isElectronShell() && hasDesktopInvoke();
export const invokeDesktop = async <T = unknown>(command: string, args?: Record<string, unknown>): Promise<T | null> => {
@@ -6,7 +6,6 @@ import {
normalizeDesktopWindowControlsPosition,
resolveDesktopWindowControlsSide,
} from './desktop';
import { useUIStore } from '@/stores/useUIStore';
describe('desktop window controls position', () => {
test('defaults to right', () => {
@@ -31,24 +30,3 @@ describe('desktop window controls position', () => {
expect(getDesktopWindowControlsOrder('right')).toEqual(['minimize', 'maximize', 'close']);
});
});
describe('useUIStore v12→v13 migration', () => {
test('introduces classic style while preserving position', () => {
const migrate = useUIStore.persist.getOptions().migrate!;
const result = migrate({ desktopWindowControlsPosition: 'left' }, 12) as Record<string, unknown>;
expect(result.desktopWindowControlsPosition).toBe('left');
expect(result.desktopWindowControlsStyle).toBe('classic');
});
test('does not overwrite a persisted style', () => {
const migrate = useUIStore.persist.getOptions().migrate!;
const result = migrate({ desktopWindowControlsStyle: 'traffic-lights' }, 12) as Record<string, unknown>;
expect(result.desktopWindowControlsStyle).toBe('traffic-lights');
});
test('coerces an invalid persisted style to classic', () => {
const migrate = useUIStore.persist.getOptions().migrate!;
const result = migrate({ desktopWindowControlsStyle: 'macos' }, 12) as Record<string, unknown>;
expect(result.desktopWindowControlsStyle).toBe('classic');
});
});
@@ -945,14 +945,14 @@ export const settingsDict = {
'settings.openchamber.sessionRetention.toast.failedArchiveCount': 'Failed to archive {count} session(s)',
'settings.openchamber.sessionRetention.toast.failedDeleteCount': 'Failed to delete {count} session(s)',
'settings.openchamber.desktopNetwork.title': 'Desktop Network Access',
'settings.openchamber.desktopNetwork.field.windowControls': 'Window controls',
'settings.openchamber.desktopNetwork.field.windowControlsDescription': 'Choose where window controls appear and how they look.',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Position',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Window controls position',
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': 'Choose where minimize, maximize, and close buttons appear. Defaults to the right.',
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'Window controls position',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Style',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'Window controls style',
'settings.openchamber.desktopNetwork.option.windowControlsLeft': 'Left',
'settings.openchamber.desktopNetwork.option.windowControlsRight': 'Right',
'settings.openchamber.desktopNetwork.field.windowControls': 'Window controls',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Style',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'Window controls style',
'settings.openchamber.desktopNetwork.option.windowControlsClassic': 'Classic',
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': 'Traffic lights',
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Start OpenChamber at login',
@@ -912,14 +912,14 @@ export const settingsDict = {
"settings.openchamber.sessionRetention.toast.failedArchiveCount": "No se pudo archivar {count} sesión(es)",
"settings.openchamber.sessionRetention.toast.failedDeleteCount": "No se pudo eliminar {count} sesión(es)",
"settings.openchamber.desktopNetwork.title": "Acceso de red de escritorio",
"settings.openchamber.desktopNetwork.field.windowControls": "Controles de ventana",
"settings.openchamber.desktopNetwork.field.windowControlsDescription": "Elige dónde aparecen los controles de ventana y su apariencia.",
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Posición",
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Posición de los controles de ventana",
"settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Elige dónde aparecen los botones de minimizar, maximizar y cerrar. Por defecto a la derecha.",
"settings.openchamber.desktopNetwork.field.windowControlsPositionAria": "Posición de los controles de ventana",
"settings.openchamber.desktopNetwork.field.windowControlsStyle": "Estilo",
"settings.openchamber.desktopNetwork.field.windowControlsStyleAria": "Estilo de los controles de ventana",
"settings.openchamber.desktopNetwork.option.windowControlsLeft": "Izquierda",
"settings.openchamber.desktopNetwork.option.windowControlsRight": "Derecha",
"settings.openchamber.desktopNetwork.field.windowControls": "Controles de ventana",
"settings.openchamber.desktopNetwork.field.windowControlsStyle": "Estilo",
"settings.openchamber.desktopNetwork.field.windowControlsStyleAria": "Estilo de los controles de ventana",
"settings.openchamber.desktopNetwork.option.windowControlsClassic": "Clásico",
"settings.openchamber.desktopNetwork.option.windowControlsTrafficLights": "Semáforo",
"settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar OpenChamber al iniciar sesión",
@@ -833,14 +833,14 @@ export const settingsDict = {
'settings.openchamber.sessionRetention.toast.failedArchiveCount': 'Échec de l\'archivage des sessions {count}',
'settings.openchamber.sessionRetention.toast.failedDeleteCount': 'Échec de la suppression des sessions {count}',
'settings.openchamber.desktopNetwork.title': 'Accès au réseau de bureau',
'settings.openchamber.desktopNetwork.field.windowControls': 'Contrôles de fenêtre',
'settings.openchamber.desktopNetwork.field.windowControlsDescription': 'Choisissez où apparaissent les contrôles de fenêtre et leur apparence.',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Position',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Position des contrôles de fenêtre',
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': 'Choisissez où apparaissent les boutons Réduire, Agrandir et Fermer. Par défaut à droite.',
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'Position des contrôles de fenêtre',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Style',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'Style des contrôles de fenêtre',
'settings.openchamber.desktopNetwork.option.windowControlsLeft': 'Gauche',
'settings.openchamber.desktopNetwork.option.windowControlsRight': 'Droite',
'settings.openchamber.desktopNetwork.field.windowControls': 'Contrôles de fenêtre',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Style',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'Style des contrôles de fenêtre',
'settings.openchamber.desktopNetwork.option.windowControlsClassic': 'Classique',
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': 'Feux de circulation',
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Démarrez OpenChamber lors de la connexion',
@@ -945,14 +945,14 @@ export const settingsDict = {
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '{count} 個の Session のアーカイブに失敗しました',
'settings.openchamber.sessionRetention.toast.failedDeleteCount': '{count} 個の Session の削除に失敗しました',
'settings.openchamber.desktopNetwork.title': 'Desktop ネットワークアクセス',
'settings.openchamber.desktopNetwork.field.windowControls': 'ウィンドウコントロール',
'settings.openchamber.desktopNetwork.field.windowControlsDescription': 'ウィンドウコントロールの表示位置とスタイルを選びます。',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '位置',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'ウィンドウコントロールの位置',
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '最小化・最大化・閉じるボタンの表示位置を選びます。デフォルトは右側です。',
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'ウィンドウコントロールの位置',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'スタイル',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'ウィンドウコントロールのスタイル',
'settings.openchamber.desktopNetwork.option.windowControlsLeft': '左',
'settings.openchamber.desktopNetwork.option.windowControlsRight': '右',
'settings.openchamber.desktopNetwork.field.windowControls': 'ウィンドウコントロール',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'スタイル',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'ウィンドウコントロールのスタイル',
'settings.openchamber.desktopNetwork.option.windowControlsClassic': 'クラシック',
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': 'トラフィックライト',
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'ログイン時に OpenChamber を起動',
@@ -912,14 +912,14 @@ export const settingsDict = {
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '세션 {count}개를 보관하지 못했습니다',
'settings.openchamber.sessionRetention.toast.failedDeleteCount': '세션 {count}개를 삭제하지 못했습니다',
'settings.openchamber.desktopNetwork.title': 'Desktop 네트워크 접속',
'settings.openchamber.desktopNetwork.field.windowControls': '창 컨트롤',
'settings.openchamber.desktopNetwork.field.windowControlsDescription': '창 컨트롤의 위치와 모양을 선택합니다.',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '위치',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '창 컨트롤 위치',
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '최소화, 최대화, 닫기 버튼이 표시될 위치를 선택합니다. 기본값은 오른쪽입니다.',
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': '창 컨트롤 위치',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': '스타일',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': '창 컨트롤 스타일',
'settings.openchamber.desktopNetwork.option.windowControlsLeft': '왼쪽',
'settings.openchamber.desktopNetwork.option.windowControlsRight': '오른쪽',
'settings.openchamber.desktopNetwork.field.windowControls': '창 컨트롤',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': '스타일',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': '창 컨트롤 스타일',
'settings.openchamber.desktopNetwork.option.windowControlsClassic': '클래식',
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': '트래픽 라이트',
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '로그인 시 OpenChamber 시작',
@@ -780,14 +780,14 @@ export const settingsDict = {
'settings.openchamber.desktopNetwork.hint.openAfterRestart': 'Po restarcie otwórz z innego urządzenia: ',
'settings.openchamber.desktopNetwork.hint.openNow': 'Otwórz z innego urządzenia: ',
'settings.openchamber.desktopNetwork.title': 'Dostęp sieciowy pulpitu',
'settings.openchamber.desktopNetwork.field.windowControls': 'Elementy sterujące oknem',
'settings.openchamber.desktopNetwork.field.windowControlsDescription': 'Wybierz, gdzie mają się pojawiać elementy sterujące oknem i jak mają wyglądać.',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Pozycja',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Pozycja elementów sterujących oknem',
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': 'Wybierz, gdzie mają się pojawiać przyciski minimalizacji, maksymalizacji i zamykania. Domyślnie po prawej.',
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'Pozycja elementów sterujących oknem',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Styl',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'Styl elementów sterujących oknem',
'settings.openchamber.desktopNetwork.option.windowControlsLeft': 'Lewo',
'settings.openchamber.desktopNetwork.option.windowControlsRight': 'Prawo',
'settings.openchamber.desktopNetwork.field.windowControls': 'Elementy sterujące oknem',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Styl',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'Styl elementów sterujących oknem',
'settings.openchamber.desktopNetwork.option.windowControlsClassic': 'Klasyczny',
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': 'Sygnalizacja świetlna',
'settings.openchamber.git.changesViewAria': 'Tryb widoku zmian Git',
@@ -912,14 +912,14 @@ export const settingsDict = {
"settings.openchamber.sessionRetention.toast.failedArchiveCount": "Não foi possível arquivar {count} sessão(es)",
"settings.openchamber.sessionRetention.toast.failedDeleteCount": "Não foi possível excluir {count} sessão(es)",
"settings.openchamber.desktopNetwork.title": "Acesso de rede do desktop",
"settings.openchamber.desktopNetwork.field.windowControls": "Controles da janela",
"settings.openchamber.desktopNetwork.field.windowControlsDescription": "Escolha onde os controles da janela aparecem e como eles são exibidos.",
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Posição",
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Posição dos controles da janela",
"settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Escolha onde os botões de minimizar, maximizar e fechar aparecem. O padrão é à direita.",
"settings.openchamber.desktopNetwork.field.windowControlsPositionAria": "Posição dos controles da janela",
"settings.openchamber.desktopNetwork.field.windowControlsStyle": "Estilo",
"settings.openchamber.desktopNetwork.field.windowControlsStyleAria": "Estilo dos controles da janela",
"settings.openchamber.desktopNetwork.option.windowControlsLeft": "Esquerda",
"settings.openchamber.desktopNetwork.option.windowControlsRight": "Direita",
"settings.openchamber.desktopNetwork.field.windowControls": "Controles da janela",
"settings.openchamber.desktopNetwork.field.windowControlsStyle": "Estilo",
"settings.openchamber.desktopNetwork.field.windowControlsStyleAria": "Estilo dos controles da janela",
"settings.openchamber.desktopNetwork.option.windowControlsClassic": "Clássico",
"settings.openchamber.desktopNetwork.option.windowControlsTrafficLights": "Semáforo",
"settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar o OpenChamber ao fazer login",
@@ -912,14 +912,14 @@ export const settingsDict = {
"settings.openchamber.sessionRetention.toast.failedArchiveCount": "Не вдалося заархівувати сесій: {count}",
"settings.openchamber.sessionRetention.toast.failedDeleteCount": "Не вдалося видалити сесій: {count}",
"settings.openchamber.desktopNetwork.title": "Мережевий доступ десктопного застосунку",
"settings.openchamber.desktopNetwork.field.windowControls": "Елементи керування вікном",
"settings.openchamber.desktopNetwork.field.windowControlsDescription": "Виберіть, де з’являються елементи керування вікном і як вони виглядають.",
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Позиція",
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Позиція елементів керування вікном",
"settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Виберіть, де з’являються кнопки згортання, розгортання та закриття. За замовчуванням справа.",
"settings.openchamber.desktopNetwork.field.windowControlsPositionAria": "Позиція елементів керування вікном",
"settings.openchamber.desktopNetwork.field.windowControlsStyle": "Стиль",
"settings.openchamber.desktopNetwork.field.windowControlsStyleAria": "Стиль елементів керування вікном",
"settings.openchamber.desktopNetwork.option.windowControlsLeft": "Зліва",
"settings.openchamber.desktopNetwork.option.windowControlsRight": "Справа",
"settings.openchamber.desktopNetwork.field.windowControls": "Елементи керування вікном",
"settings.openchamber.desktopNetwork.field.windowControlsStyle": "Стиль",
"settings.openchamber.desktopNetwork.field.windowControlsStyleAria": "Стиль елементів керування вікном",
"settings.openchamber.desktopNetwork.option.windowControlsClassic": "Класичний",
"settings.openchamber.desktopNetwork.option.windowControlsTrafficLights": "Світлофор",
"settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Запускати OpenChamber під час входу в систему",
@@ -912,14 +912,14 @@ export const settingsDict = {
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '归档 {count} 个会话失败',
'settings.openchamber.sessionRetention.toast.failedDeleteCount': '删除 {count} 个会话失败',
'settings.openchamber.desktopNetwork.title': '桌面端网络访问',
'settings.openchamber.desktopNetwork.field.windowControls': '窗口控件',
'settings.openchamber.desktopNetwork.field.windowControlsDescription': '选择窗口控件的位置和外观。',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '位置',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '窗口控件位置',
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '选择最小化、最大化和关闭按钮的显示位置。默认在右侧。',
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': '窗口控件位置',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': '样式',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': '窗口控件样式',
'settings.openchamber.desktopNetwork.option.windowControlsLeft': '左侧',
'settings.openchamber.desktopNetwork.option.windowControlsRight': '右侧',
'settings.openchamber.desktopNetwork.field.windowControls': '窗口控件',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': '样式',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': '窗口控件样式',
'settings.openchamber.desktopNetwork.option.windowControlsClassic': '经典',
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': '红绿灯',
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '登录时启动 OpenChamber',
@@ -909,14 +909,14 @@
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '封存 {count} 個工作階段失敗',
'settings.openchamber.sessionRetention.toast.failedDeleteCount': '刪除 {count} 個工作階段失敗',
'settings.openchamber.desktopNetwork.title': '桌面端網路存取',
'settings.openchamber.desktopNetwork.field.windowControls': '視窗控制項',
'settings.openchamber.desktopNetwork.field.windowControlsDescription': '選擇視窗控制項的位置和外觀。',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '位置',
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '視窗控制項位置',
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '選擇最小化、最大化和關閉按鈕的顯示位置。預設在右側。',
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': '視窗控制項位置',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': '樣式',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': '視窗控制項樣式',
'settings.openchamber.desktopNetwork.option.windowControlsLeft': '左側',
'settings.openchamber.desktopNetwork.option.windowControlsRight': '右側',
'settings.openchamber.desktopNetwork.field.windowControls': '視窗控制項',
'settings.openchamber.desktopNetwork.field.windowControlsStyle': '樣式',
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': '視窗控制項樣式',
'settings.openchamber.desktopNetwork.option.windowControlsClassic': '經典',
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': '紅綠燈',
'settings.openchamber.desktopNetwork.field.allowLanAccessAria': '允許桌面 sidecar 區域網路存取',
+3 -3
View File
@@ -399,9 +399,9 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
{
id: 'sessions.desktop-window-controls-position',
page: 'appearance',
titleKey: 'settings.openchamber.desktopNetwork.field.windowControls',
descriptionKey: 'settings.openchamber.desktopNetwork.field.windowControlsDescription',
keywords: ['desktop', 'window', 'controls', 'minimize', 'maximize', 'close', 'titlebar', 'linux', 'windows', 'position'],
titleKey: 'settings.openchamber.desktopNetwork.field.windowControlsPosition',
descriptionKey: 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription',
keywords: ['desktop', 'window', 'controls', 'minimize', 'maximize', 'close', 'titlebar', 'linux', 'windows'],
isAvailable: (ctx) => ctx.isDesktop && (ctx.isWindows || !ctx.isMac),
},
{
-2
View File
@@ -13,7 +13,6 @@ import { applyPersistedDirectoryPreferences } from './lib/directoryPersistence'
import { startTypographyWatcher } from './lib/typographyWatcher'
import { startModelPrefsAutoSave } from './lib/modelPrefsAutoSave'
import { initializeLocale, I18nProvider } from './lib/i18n'
import { initLinuxWindowBorder } from './lib/desktop'
import type { RuntimeAPIs } from './lib/api/types'
declare global {
@@ -27,7 +26,6 @@ const runtimeAPIs = (typeof window !== 'undefined' && window.__OPENCHAMBER_RUNTI
})();
initializeLocale();
initLinuxWindowBorder();
// Initialize settings asynchronously — the app renders with defaults first
// and hydrates once persisted preferences are applied. Users with non-default
+1 -10
View File
@@ -2254,7 +2254,7 @@ export const useUIStore = create<UIStore>()(
{
name: 'ui-store',
storage: createDeferredSafeJSONStorage(),
version: 13,
version: 12,
migrate: (persistedState, version) => {
if (!persistedState || typeof persistedState !== 'object') {
return persistedState;
@@ -2268,15 +2268,6 @@ export const useUIStore = create<UIStore>()(
}
}
// v12 -> v13: introduce window-controls style. Existing users keep
// their persisted position; style defaults to "classic" so no one
// suddenly sees traffic lights on upgrade.
if (version < 13) {
if (state.desktopWindowControlsStyle !== 'classic' && state.desktopWindowControlsStyle !== 'traffic-lights') {
state.desktopWindowControlsStyle = 'classic';
}
}
// v10 -> v11: move the previous terminal font default forward.
if (version < 11 && state.terminalFontSize === 13) {
state.terminalFontSize = 14;