From 3541bc63f98b453230efa5b6d0399d15795450a7 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Tue, 9 Jun 2026 18:38:01 +0300 Subject: [PATCH] feat(desktop): macOS vibrancy for the left sidebar with a toggle Add native macOS vibrancy behind the left sidebar (the only translucent surface; header/chat/right sidebar stay opaque), plus a setting to turn it off. - Window created with vibrancy applied after first show (avoids the cold-launch no-composite quirk); minimize/restore suppress the frost during the genie animation. Renderer frosts the sidebar via --sidebar-vibrancy-overlay once data-oc-vibrancy[-ready] are set; project-actions pill matches when open. - data-oc-vibrancy-ready defaults are set in cssGenerator (DOM guaranteed), not the preload (document-start race left the sidebar un-frosted on launch). - prefers-reduced-transparency falls back to solid surfaces. - Appearance settings (macOS desktop only): a checkbox to enable/disable vibrancy, persisted to settings.json and applied via a Save & restart button (vibrancy is a window-creation option, so it needs a relaunch). --- packages/electron/main.mjs | 83 +++++++++++++++++-- packages/electron/preload.mjs | 22 +++++ packages/ui/src/components/layout/Sidebar.tsx | 2 +- .../layout/TitlebarLeftControls.tsx | 5 ++ .../openchamber/OpenChamberVisualSettings.tsx | 62 +++++++++++++- .../ui/src/contexts/ThemeSystemContext.tsx | 4 +- packages/ui/src/index.css | 57 +++++++++++++ packages/ui/src/lib/desktop.ts | 2 + .../ui/src/lib/i18n/messages/en.settings.ts | 4 + .../ui/src/lib/i18n/messages/es.settings.ts | 4 + .../ui/src/lib/i18n/messages/ko.settings.ts | 4 + .../ui/src/lib/i18n/messages/pl.settings.ts | 4 + .../src/lib/i18n/messages/pt-BR.settings.ts | 4 + .../ui/src/lib/i18n/messages/uk.settings.ts | 4 + .../src/lib/i18n/messages/zh-CN.settings.ts | 4 + .../src/lib/i18n/messages/zh-TW.settings.ts | 4 + packages/ui/src/lib/theme/cssGenerator.ts | 22 +++++ packages/ui/src/styles/design-system.css | 7 ++ packages/ui/src/types/desktop.d.ts | 2 +- 19 files changed, 288 insertions(+), 12 deletions(-) diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 66f5d54c..24642f7f 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -1480,6 +1480,36 @@ const emitToAllWindows = (event, detail) => { } }; +// macOS vibrancy: the native NSVisualEffectView needs a moment to settle after +// the window is shown/restored. Until then the renderer keeps the sidebar solid +// to avoid a flash of raw transparency; once ready it switches to the +// translucent overlay. We toggle this readiness over the same IPC bridge. +// Apply vibrancy to a live, on-screen window. Done after show (not in the +// BrowserWindow constructor) because macOS otherwise leaves the material +// uncomposited on a cold launch until the window gets a state change. +const applyMacVibrancy = (browserWindow) => { + if (process.platform !== 'darwin' || !browserWindow || browserWindow.isDestroyed()) return; + try { + browserWindow.setVibrancy('sidebar'); + } catch {} +}; + +const setMacVibrancyReady = (browserWindow, ready) => { + if (process.platform !== 'darwin' || !browserWindow || browserWindow.isDestroyed()) return; + emitToWindow(browserWindow, 'openchamber:vibrancy-ready', { ready }); +}; + +const scheduleMacVibrancyReady = (browserWindow, delayMs = 160) => { + if (process.platform !== 'darwin' || !browserWindow || browserWindow.isDestroyed()) return; + setMacVibrancyReady(browserWindow, false); + const timer = setTimeout(() => { + if (browserWindow.isDestroyed() || browserWindow.isMinimized() || !browserWindow.isVisible()) return; + setMacVibrancyReady(browserWindow, true); + }, delayMs); + if (typeof timer?.unref === 'function') timer.unref(); +}; + + const setTaskbarProgress = (value) => { if (process.platform !== 'win32') return; for (const browserWindow of BrowserWindow.getAllWindows()) { @@ -1772,6 +1802,8 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } const desktopHome = os.homedir() || ''; const desktopMacosMajor = String(macosMajorVersion()); const usesCustomTitleBar = process.platform === 'darwin' || process.platform === 'win32'; + // macOS vibrancy, on by default; users can disable it (Appearance settings). + const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false; const titleBarOverlayEnabled = false; const autoHidesNativeMenuBar = process.platform !== 'darwin'; const windowIconPath = getWindowIconPath(); @@ -1786,7 +1818,11 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } minHeight: MIN_WINDOW_HEIGHT, icon: windowIconPath, show: false, - backgroundColor: '#151313', + backgroundColor: useVibrancy ? '#00000000' : '#151313', + // Vibrancy is applied after the window is shown (see applyMacVibrancy), not + // here: setting it in the constructor leaves the material uncomposited on a + // cold launch until a window event. No `transparent: true` either — vibrancy + // alone is enough and composites reliably once applied to a live window. frame: process.platform === 'win32' ? false : undefined, autoHideMenuBar: autoHidesNativeMenuBar, // Electron's hiddenInset adds its own extra inset, which leaves the controls @@ -1801,6 +1837,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } `--openchamber-client-token=${desktopClientToken}`, `--openchamber-home=${desktopHome}`, `--openchamber-macos-major=${desktopMacosMajor}`, + `--openchamber-mac-vibrancy=${useVibrancy ? '1' : '0'}`, `--openchamber-boot-outcome=${JSON.stringify(state.bootOutcome || null)}`, ], preload: isDev ? path.join(__dirname, 'preload.mjs') : path.join(app.getAppPath(), 'preload.mjs'), @@ -1847,11 +1884,20 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } browserWindow.setTrafficLightPosition({ x: 16, y: 17 }); } catch {} }; - browserWindow.on('minimize', refreshTrafficLights); + browserWindow.on('minimize', () => { + refreshTrafficLights(); + setMacVibrancyReady(browserWindow, false); + }); browserWindow.on('restore', () => { refreshTrafficLights(); setTimeout(refreshTrafficLights, 250); + scheduleMacVibrancyReady(browserWindow, 180); }); + // Only suppress vibrancy around the minimize/restore cycle (it flashes raw + // transparency during the genie animation). A plain show — cold launch from + // the dock, un-hide — must NOT suppress, or the sidebar gets stuck solid + // when the post-show `ready` re-enable is skipped while the window is still + // animating in. browserWindow.on('show', refreshTrafficLights); browserWindow.on('focus', refreshTrafficLights); } @@ -1976,6 +2022,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } browserWindow.once('ready-to-show', () => { browserWindow.show(); browserWindow.focus(); + if (useVibrancy) applyMacVibrancy(browserWindow); }); if (url) { @@ -2103,6 +2150,8 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj const desktopClientToken = effectiveRuntimeConfig.clientToken || ''; const desktopHome = os.homedir() || ''; const desktopMacosMajor = String(macosMajorVersion()); + // macOS vibrancy, on by default; users can disable it (Appearance settings). + const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false; const browserWindow = new BrowserWindow({ title: 'OpenChamber Mini Chat', width: MINI_CHAT_WINDOW_WIDTH, @@ -2111,7 +2160,11 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj minHeight: MINI_CHAT_MIN_WINDOW_HEIGHT, icon: getWindowIconPath(), show: false, - backgroundColor: '#151313', + backgroundColor: useVibrancy ? '#00000000' : '#151313', + // Vibrancy is applied after the window is shown (see applyMacVibrancy), not + // here: setting it in the constructor leaves the material uncomposited on a + // cold launch until a window event. No `transparent: true` either — vibrancy + // alone is enough and composites reliably once applied to a live window. frame: process.platform === 'win32' ? false : undefined, autoHideMenuBar: process.platform !== 'darwin', titleBarStyle: process.platform === 'darwin' || process.platform === 'win32' ? 'hidden' : 'default', @@ -2161,13 +2214,17 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj browserWindow.setTrafficLightPosition({ x: 16, y: 17 }); } catch {} }; + // Suppress vibrancy only around minimize/restore, never on a plain show. browserWindow.on('show', refreshTrafficLights); browserWindow.on('focus', refreshTrafficLights); + browserWindow.on('minimize', () => setMacVibrancyReady(browserWindow, false)); + browserWindow.on('restore', () => scheduleMacVibrancyReady(browserWindow, 180)); } browserWindow.once('ready-to-show', () => { browserWindow.show(); browserWindow.focus(); + if (useVibrancy) applyMacVibrancy(browserWindow); }); browserWindow.webContents.setWindowOpenHandler(({ url }) => { @@ -3309,13 +3366,23 @@ const handleInvoke = async (browserWindow, command, args = {}) => { } case 'desktop_set_vibrancy': { - // Vibrancy (macOS blur) is not supported in the Electron shell for our - // titleBarStyle:'hidden' setup. Persist the - // disabled state so settings UI reflects it; args.enabled is ignored. + // Vibrancy + transparent backing are window-creation options, so the + // change only takes effect on a fresh launch. Persist the preference, + // then relaunch the app. + const enabled = args.enabled === true; await mutateSettingsRoot((root) => { - root.desktopVibrancy = false; + root.desktopVibrancy = enabled; }); - return { enabled: false, requiresRestart: false }; + setImmediate(() => { + try { + prepareForQuit(); + app.relaunch(); + app.exit(0); + } catch (err) { + log.error('[electron] desktop_set_vibrancy relaunch failed', err); + } + }); + return { enabled, requiresRestart: true }; } case 'desktop_check_for_updates': { diff --git a/packages/electron/preload.mjs b/packages/electron/preload.mjs index 6a2fb9b3..3e917d5b 100644 --- a/packages/electron/preload.mjs +++ b/packages/electron/preload.mjs @@ -17,6 +17,10 @@ const clientToken = readArgValue('--openchamber-client-token'); const homeDirectory = readArgValue('--openchamber-home'); const macosMajorRaw = readArgValue('--openchamber-macos-major'); const macosMajor = Number.parseInt(macosMajorRaw, 10); +const macVibrancySupported = process.platform === 'darwin'; +// Effective state for this window (main process resolves the saved preference +// and passes it in). Defaults on when supported unless explicitly '0'. +const hasMacVibrancy = macVibrancySupported && readArgValue('--openchamber-mac-vibrancy') !== '0'; // Preload re-executes on every cross-origin navigation (we run with // sandbox:false, per-document). Two separate concerns to balance: @@ -72,6 +76,8 @@ if (Number.isFinite(macosMajor) && macosMajor > 0) { contextBridge.exposeInMainWorld('__OPENCHAMBER_ELECTRON__', { runtime: 'electron', + macVibrancy: hasMacVibrancy, + macVibrancySupported, }); contextBridge.exposeInMainWorld('__OPENCHAMBER_PLATFORM__', process.platform); @@ -120,6 +126,18 @@ const dispatchNativeEvent = (event, detail) => { } }; +// Toggles the frost on/off in response to the main process around the +// minimize/restore cycle. The default ("ready") state is set reliably in the +// renderer (cssGenerator) — not here — because this preload runs at +// document-start when documentElement may not exist yet. +const setVibrancyReady = (ready) => { + if (!hasMacVibrancy) return; + try { + document.documentElement.toggleAttribute('data-oc-vibrancy-ready', ready === true); + } catch { + } +}; + // Main-process events are read-only notifications (update progress, // window focus, etc.) — safe to deliver to any page rendered in this // webContents. The events themselves don't grant capability. @@ -133,6 +151,10 @@ ipcRenderer.on('openchamber:emit', (_evt, payload) => { return; } + if (event === 'openchamber:vibrancy-ready') { + setVibrancyReady(payload.detail?.ready === true); + } + dispatchNativeEvent(event, payload.detail); }); diff --git a/packages/ui/src/components/layout/Sidebar.tsx b/packages/ui/src/components/layout/Sidebar.tsx index 65e89059..41a48f74 100644 --- a/packages/ui/src/components/layout/Sidebar.tsx +++ b/packages/ui/src/components/layout/Sidebar.tsx @@ -127,7 +127,7 @@ export const Sidebar: React.FC = ({ isOpen, isMobile, children, cl ref={sidebarRef} className={cn( 'relative flex h-full overflow-hidden border-r border-border/40 will-change-[width] motion-reduce:transition-none', - 'bg-sidebar', + 'bg-sidebar oc-vibrancy-surface', !isOpen && 'border-r-0', className, )} diff --git a/packages/ui/src/components/layout/TitlebarLeftControls.tsx b/packages/ui/src/components/layout/TitlebarLeftControls.tsx index a3b22ca4..33636d56 100644 --- a/packages/ui/src/components/layout/TitlebarLeftControls.tsx +++ b/packages/ui/src/components/layout/TitlebarLeftControls.tsx @@ -25,6 +25,7 @@ const ICON_BUTTON_CLASS = export const TitlebarLeftControls: React.FC = () => { const { t } = useI18n(); const toggleSidebar = useUIStore((state) => state.toggleSidebar); + const isSidebarOpen = useUIStore((state) => state.isSidebarOpen); const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); const projectActionsContext = useProjectActionsContext(); const clusterRef = React.useRef(null); @@ -90,6 +91,10 @@ export const TitlebarLeftControls: React.FC = () => { ) : null} diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 44608ce7..fd6853b8 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -20,7 +20,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Icon } from "@/components/icon/Icon"; -import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; +import { invokeDesktop, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; import { useDeviceInfo } from '@/lib/device'; import { usePwaDetection } from '@/hooks/usePwaDetection'; import { updateDesktopSettings } from '@/lib/persistence'; @@ -339,6 +339,16 @@ export const OpenChamberVisualSettings: React.FC } = useThemeSystem(); const [themesReloading, setThemesReloading] = React.useState(false); + + // macOS-desktop-only vibrancy toggle. Changing it needs a full relaunch + // (vibrancy is a window-creation option), so we persist + restart on save. + const macVibrancySupported = React.useMemo( + () => isDesktopShell() && typeof window !== 'undefined' && window.__OPENCHAMBER_ELECTRON__?.macVibrancySupported === true, + [], + ); + const macVibrancyEnabled = typeof window !== 'undefined' && window.__OPENCHAMBER_ELECTRON__?.macVibrancy === true; + const [vibrancyChecked, setVibrancyChecked] = React.useState(macVibrancyEnabled); + const [vibrancyRestarting, setVibrancyRestarting] = React.useState(false); const [chatRenderPreviewTick, setChatRenderPreviewTick] = React.useState(0); const reportUsage = useUIStore(state => state.reportUsage); const setReportUsage = useUIStore(state => state.setReportUsage); @@ -797,6 +807,56 @@ export const OpenChamberVisualSettings: React.FC + + {macVibrancySupported && ( +
+
{ if (!vibrancyRestarting) setVibrancyChecked(!vibrancyChecked); }} + onKeyDown={(event) => { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + if (!vibrancyRestarting) setVibrancyChecked(!vibrancyChecked); + } + }} + > + +
+ + {t('settings.openchamber.visual.field.macVibrancy')} + + + {t('settings.openchamber.visual.field.macVibrancyHint')} + +
+
+ {vibrancyChecked !== macVibrancyEnabled && ( +
+ +
+ )} +
+ )} )} diff --git a/packages/ui/src/contexts/ThemeSystemContext.tsx b/packages/ui/src/contexts/ThemeSystemContext.tsx index 9db7bfa4..c1e213b6 100644 --- a/packages/ui/src/contexts/ThemeSystemContext.tsx +++ b/packages/ui/src/contexts/ThemeSystemContext.tsx @@ -345,7 +345,9 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro if (typeof document === 'undefined') { return; } - const chromeColor = theme.colors.surface.background; + const hasMacVibrancy = document.documentElement.hasAttribute('data-oc-vibrancy') + || window.__OPENCHAMBER_ELECTRON__?.macVibrancy === true; + const chromeColor = hasMacVibrancy ? 'transparent' : theme.colors.surface.background; document.body.style.backgroundColor = chromeColor; diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index 3d0346d8..dc4de7f0 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -40,6 +40,63 @@ button, animation: none !important; } +/* ── macOS vibrancy ─────────────────────────────────────────────────────── + The window is created transparent with a native 'sidebar' NSVisualEffectView + behind it. We keep the whole app opaque (bg-background covers the window) + EXCEPT the left sidebar, which is the only translucent surface that lets the + native vibrancy show through. `data-oc-vibrancy` = vibrancy active; + `data-oc-vibrancy-ready` = native layer has settled (set from the main + process after show/restore) — until then the sidebar stays solid to avoid a + flash of raw transparency. */ +html[data-oc-vibrancy] body, +html[data-oc-vibrancy] #root { + background: transparent !important; + background-color: transparent !important; +} + +html[data-oc-vibrancy] .main-content-safe-area { + background: transparent !important; + background-color: transparent !important; +} + +/* 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 { + background: var(--sidebar) !important; +} + +html[data-oc-vibrancy][data-oc-vibrancy-ready] .oc-vibrancy-surface { + background: var(--sidebar-vibrancy-overlay) !important; +} + +/* Inner bg-sidebar elements inside the vibrant surface must not re-paint an + opaque fill, or they'd cover the frosted effect. */ +html[data-oc-vibrancy] .oc-vibrancy-surface .bg-sidebar { + background: transparent !important; + background-color: transparent !important; +} + +/* Project-actions pill over the open sidebar: drop its opaque surface so it + frosts to the exact same level as the sidebar (border keeps its outline). */ +html[data-oc-vibrancy] .oc-vibrancy-pill { + background: transparent !important; + background-color: transparent !important; +} + +/* Accessibility: drop translucency when the OS asks for reduced transparency. */ +@media (prefers-reduced-transparency: reduce) { + html[data-oc-vibrancy] body, + html[data-oc-vibrancy] #root, + html[data-oc-vibrancy] .main-content-safe-area { + background: var(--surface-background) !important; + } + + html[data-oc-vibrancy] .oc-vibrancy-surface, + html[data-oc-vibrancy][data-oc-vibrancy-ready] .oc-vibrancy-surface { + background: var(--sidebar) !important; + } +} + /* Suppress WebKit-specific hover/focus adornments on the chat textarea */ textarea[data-chat-input="true"] { -webkit-appearance: none; diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index 3a2e47fd..e07bd71d 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -201,6 +201,8 @@ type DesktopBridgeGlobal = { type ElectronRuntimeGlobal = { runtime?: string; + macVibrancy?: boolean; + macVibrancySupported?: boolean; }; const getElectronRuntime = (): ElectronRuntimeGlobal | null => { diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 39a22a55..ee673ea8 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1573,6 +1573,10 @@ export const settingsDict = { 'settings.openchamber.visual.field.selectWeekStartAria': 'Select week start', 'settings.openchamber.visual.actions.reloadThemes': 'Reload themes', 'settings.openchamber.visual.actions.reloadingThemes': 'Reloading themes...', + 'settings.openchamber.visual.field.macVibrancy': 'Window transparency', + 'settings.openchamber.visual.field.macVibrancyHint': 'Use the native macOS blur (vibrancy) behind the sidebar. Turn off for fully solid, opaque surfaces.', + 'settings.openchamber.visual.actions.saveAndRestart': 'Save & restart', + 'settings.openchamber.visual.actions.restarting': 'Restarting…', 'settings.openchamber.visual.field.themeImportInfoAria': 'Theme import info', 'settings.openchamber.visual.field.themeImportInfoTooltip': 'Import custom themes from ~/.config/openchamber/themes/', 'settings.openchamber.visual.field.installAppName': 'Install App Name', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index a22f52c5..02478da2 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1539,6 +1539,10 @@ export const settingsDict = { "settings.openchamber.visual.field.selectTimeFormatAria": "Seleccionar formato de hora", "settings.openchamber.visual.field.selectWeekStartAria": "Seleccionar inicio de la semana", "settings.openchamber.visual.actions.reloadThemes": "Recargar temas", + "settings.openchamber.visual.field.macVibrancy": "Transparencia de la ventana", + "settings.openchamber.visual.field.macVibrancyHint": "Usa el desenfoque nativo de macOS (vibrancy) detrás de la barra lateral. Desactívalo para superficies totalmente sólidas y opacas.", + "settings.openchamber.visual.actions.saveAndRestart": "Guardar y reiniciar", + "settings.openchamber.visual.actions.restarting": "Reiniciando…", "settings.openchamber.visual.actions.reloadingThemes": "Recargando temas...", "settings.openchamber.visual.field.themeImportInfoAria": "Información de importación de temas", "settings.openchamber.visual.field.themeImportInfoTooltip": "Importar temas personalizados desde ~/.config/openchamber/themes/", diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 4b983732..3ead35c2 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1539,6 +1539,10 @@ export const settingsDict = { 'settings.openchamber.visual.field.selectTimeFormatAria': '시간 형식 선택', 'settings.openchamber.visual.field.selectWeekStartAria': '주 시작 요일 선택', 'settings.openchamber.visual.actions.reloadThemes': '테마 다시 로드', + 'settings.openchamber.visual.field.macVibrancy': '창 투명도', + 'settings.openchamber.visual.field.macVibrancyHint': '사이드바 뒤에 macOS 기본 블러(vibrancy)를 사용합니다. 완전히 불투명한 표면을 원하면 끄세요.', + 'settings.openchamber.visual.actions.saveAndRestart': '저장 후 재시작', + 'settings.openchamber.visual.actions.restarting': '재시작 중…', 'settings.openchamber.visual.actions.reloadingThemes': '테마 다시 로드 중...', 'settings.openchamber.visual.field.themeImportInfoAria': '테마 가져오기 정보', 'settings.openchamber.visual.field.themeImportInfoTooltip': '~/.config/openchamber/themes/에서 사용자 정의 테마를 가져옵니다', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index fdaf8b49..7af1bfdd 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -878,6 +878,10 @@ export const settingsDict = { 'settings.openchamber.tunnel.warning.quickModeReliability': 'Aby uzyskać bardziej niezawodny długotrwały dostęp, przełącz się na tryb Managed Remote lub Managed Local.', 'settings.openchamber.tunnel.warning.replacesActiveTunnel': 'Uruchomienie tego tunelu zastępuje aktywny tunel i unieważnia istniejące linki połączenia oraz zdalne sesje.', 'settings.openchamber.visual.actions.reloadThemes': 'Przeładuj motywy', + 'settings.openchamber.visual.field.macVibrancy': 'Przezroczystość okna', + 'settings.openchamber.visual.field.macVibrancyHint': 'Używaj natywnego rozmycia macOS (vibrancy) za panelem bocznym. Wyłącz, aby uzyskać w pełni nieprzezroczyste powierzchnie.', + 'settings.openchamber.visual.actions.saveAndRestart': 'Zapisz i uruchom ponownie', + 'settings.openchamber.visual.actions.restarting': 'Ponowne uruchamianie…', 'settings.openchamber.visual.actions.reloadingThemes': 'Przeładowywanie motywów...', 'settings.openchamber.visual.actions.resetCodeFontAria': 'Zresetuj czcionkę kodu', 'settings.openchamber.visual.actions.resetFontSizeAria': 'Zresetuj rozmiar czcionki', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index d2cf9d59..fcd82608 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1539,6 +1539,10 @@ export const settingsDict = { "settings.openchamber.visual.field.selectTimeFormatAria": "Selecionar formato de hora", "settings.openchamber.visual.field.selectWeekStartAria": "Selecionar início da semana", "settings.openchamber.visual.actions.reloadThemes": "Recarregar temas", + "settings.openchamber.visual.field.macVibrancy": "Transparência da janela", + "settings.openchamber.visual.field.macVibrancyHint": "Usa o desfoque nativo do macOS (vibrancy) atrás da barra lateral. Desative para superfícies totalmente sólidas e opacas.", + "settings.openchamber.visual.actions.saveAndRestart": "Salvar e reiniciar", + "settings.openchamber.visual.actions.restarting": "Reiniciando…", "settings.openchamber.visual.actions.reloadingThemes": "Recarregando temas...", "settings.openchamber.visual.field.themeImportInfoAria": "Informações de importação de temas", "settings.openchamber.visual.field.themeImportInfoTooltip": "Importar temas personalizados de ~/.config/openchamber/themês/", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 8d6a58d0..46b29c1a 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1539,6 +1539,10 @@ export const settingsDict = { "settings.openchamber.visual.field.selectTimeFormatAria": "Вибрати формат часу", "settings.openchamber.visual.field.selectWeekStartAria": "Вибрати початок тижня", "settings.openchamber.visual.actions.reloadThemes": "Перезавантажити теми", + "settings.openchamber.visual.field.macVibrancy": "Прозорість вікна", + "settings.openchamber.visual.field.macVibrancyHint": "Використовувати нативне розмиття macOS (vibrancy) під сайдбаром. Вимкніть для повністю непрозорих поверхонь.", + "settings.openchamber.visual.actions.saveAndRestart": "Зберегти та перезапустити", + "settings.openchamber.visual.actions.restarting": "Перезапуск…", "settings.openchamber.visual.actions.reloadingThemes": "Перезавантаження тем...", "settings.openchamber.visual.field.themeImportInfoAria": "Інформація про імпорт теми", "settings.openchamber.visual.field.themeImportInfoTooltip": "Імпорт спеціальних тем із ~/.config/openchamber/themes/", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index 6d3caf3e..34fc5a41 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1539,6 +1539,10 @@ export const settingsDict = { 'settings.openchamber.visual.field.selectTimeFormatAria': '选择时间格式', 'settings.openchamber.visual.field.selectWeekStartAria': '选择一周起始日', 'settings.openchamber.visual.actions.reloadThemes': '重新加载主题', + 'settings.openchamber.visual.field.macVibrancy': '窗口透明度', + 'settings.openchamber.visual.field.macVibrancyHint': '在侧边栏后使用 macOS 原生模糊(vibrancy)。关闭以获得完全不透明的界面。', + 'settings.openchamber.visual.actions.saveAndRestart': '保存并重启', + 'settings.openchamber.visual.actions.restarting': '正在重启…', 'settings.openchamber.visual.actions.reloadingThemes': '正在重新加载主题...', 'settings.openchamber.visual.field.themeImportInfoAria': '主题导入说明', 'settings.openchamber.visual.field.themeImportInfoTooltip': '从 ~/.config/openchamber/themes/ 导入自定义主题', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 905acf5a..f4f0060e 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1458,6 +1458,10 @@ 'settings.openchamber.visual.field.selectTimeFormatAria': '選擇時間格式', 'settings.openchamber.visual.field.selectWeekStartAria': '選擇一週起始日', 'settings.openchamber.visual.actions.reloadThemes': '重新載入主題', + 'settings.openchamber.visual.field.macVibrancy': '視窗透明度', + 'settings.openchamber.visual.field.macVibrancyHint': '在側邊欄後使用 macOS 原生模糊(vibrancy)。關閉以獲得完全不透明的介面。', + 'settings.openchamber.visual.actions.saveAndRestart': '儲存並重新啟動', + 'settings.openchamber.visual.actions.restarting': '正在重新啟動…', 'settings.openchamber.visual.actions.reloadingThemes': '正在重新載入主題...', 'settings.openchamber.visual.field.themeImportInfoAria': '主題匯入說明', 'settings.openchamber.visual.field.themeImportInfoTooltip': '從 ~/.config/openchamber/themes/ 匯入自訂主題', diff --git a/packages/ui/src/lib/theme/cssGenerator.ts b/packages/ui/src/lib/theme/cssGenerator.ts index 623da99f..e609d3c2 100644 --- a/packages/ui/src/lib/theme/cssGenerator.ts +++ b/packages/ui/src/lib/theme/cssGenerator.ts @@ -123,6 +123,9 @@ const sidebarBaseRgb = hexToRgb(theme.colors.surface.muted); const isDark = theme.metadata.variant === 'dark'; const strongAlpha = isDark ? 0.15 : 0.5; const softAlpha = isDark ? 0.1 : 0.3; + // Translucent fill painted over the native macOS vibrancy layer for the + // left sidebar — high enough alpha to stay legible, low enough to frost. + const vibrancyAlpha = isDark ? 0.66 : 0.76; if (sidebarBaseRgb) { vars.push( @@ -131,6 +134,9 @@ const sidebarBaseRgb = hexToRgb(theme.colors.surface.muted); vars.push( ` --sidebar-overlay-soft: rgb(${sidebarBaseRgb} / ${softAlpha}) !important;`, ); + vars.push( + ` --sidebar-vibrancy-overlay: rgb(${sidebarBaseRgb} / ${vibrancyAlpha}) !important;`, + ); } else { const base = theme.colors.surface.muted; vars.push( @@ -139,6 +145,9 @@ const sidebarBaseRgb = hexToRgb(theme.colors.surface.muted); vars.push( ` --sidebar-overlay-soft: ${this.opacity(base, softAlpha)} !important;`, ); + vars.push( + ` --sidebar-vibrancy-overlay: ${this.opacity(base, vibrancyAlpha)} !important;`, + ); } if (theme.colors.charts?.series && Array.isArray(theme.colors.charts.series)) { @@ -186,6 +195,19 @@ const sidebarBaseRgb = hexToRgb(theme.colors.surface.muted); document.head.appendChild(style); document.documentElement.setAttribute('data-theme', theme.metadata.variant); + + const hasMacVibrancy = typeof window !== 'undefined' + && window.__OPENCHAMBER_ELECTRON__?.runtime === 'electron' + && window.__OPENCHAMBER_ELECTRON__?.macVibrancy === true; + document.documentElement.toggleAttribute('data-oc-vibrancy', hasMacVibrancy); + // Default the "ready" flag here (DOM is guaranteed to exist) rather than + // relying on the preload, which sets it at document-start when + // documentElement may not exist yet — that race left the sidebar stuck + // un-frosted on cold launch until a minimize/restore re-sent ready=true. + // The minimize/restore IPC continues to toggle this afterwards. + if (hasMacVibrancy) { + document.documentElement.toggleAttribute('data-oc-vibrancy-ready', true); + } } private generatePrimaryColors(primary: Theme['colors']['primary']): string[] { diff --git a/packages/ui/src/styles/design-system.css b/packages/ui/src/styles/design-system.css index 51732fe4..f2e2cd79 100644 --- a/packages/ui/src/styles/design-system.css +++ b/packages/ui/src/styles/design-system.css @@ -142,6 +142,13 @@ background-color: var(--background) !important; } + :root.desktop-runtime[data-oc-vibrancy], + :root.desktop-runtime[data-oc-vibrancy] body, + :root.desktop-runtime[data-oc-vibrancy] #root { + background: transparent !important; + background-color: transparent !important; + } + .font-sans { font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) !important; } diff --git a/packages/ui/src/types/desktop.d.ts b/packages/ui/src/types/desktop.d.ts index 96184fbf..2718e17d 100644 --- a/packages/ui/src/types/desktop.d.ts +++ b/packages/ui/src/types/desktop.d.ts @@ -5,7 +5,7 @@ declare global { __OPENCHAMBER_HOME__?: string; __OPENCHAMBER_MACOS_MAJOR__?: number; __OPENCHAMBER_LOCAL_ORIGIN__?: string; - __OPENCHAMBER_ELECTRON__?: { runtime?: string }; + __OPENCHAMBER_ELECTRON__?: { runtime?: string; macVibrancy?: boolean; macVibrancySupported?: boolean }; __OPENCHAMBER_PLATFORM__?: string; __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__?: DesktopBootOutcome; }