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).
This commit is contained in:
@@ -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': {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ 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,
|
||||
)}
|
||||
|
||||
@@ -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<HTMLDivElement | null>(null);
|
||||
@@ -90,6 +91,10 @@ export const TitlebarLeftControls: React.FC = () => {
|
||||
<ProjectActionsButton
|
||||
projectRef={projectActionsContext.projectRef}
|
||||
directory={projectActionsContext.directory}
|
||||
// While the sidebar is open the controls sit over the frosted
|
||||
// sidebar — let the pill share its translucency instead of painting
|
||||
// an opaque surface (handled under [data-oc-vibrancy] in CSS).
|
||||
className={isSidebarOpen ? 'oc-vibrancy-pill' : undefined}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -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<OpenChamberVisualSettingsProps>
|
||||
} = 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<OpenChamberVisualSettingsProps>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{macVibrancySupported && (
|
||||
<div className="flex flex-col gap-1.5 border-t border-border/40 pt-3">
|
||||
<div
|
||||
className="group flex cursor-pointer items-start gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={vibrancyChecked}
|
||||
onClick={() => { if (!vibrancyRestarting) setVibrancyChecked(!vibrancyChecked); }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (!vibrancyRestarting) setVibrancyChecked(!vibrancyChecked);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={vibrancyChecked}
|
||||
onChange={setVibrancyChecked}
|
||||
disabled={vibrancyRestarting}
|
||||
ariaLabel={t('settings.openchamber.visual.field.macVibrancy')}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">
|
||||
{t('settings.openchamber.visual.field.macVibrancy')}
|
||||
</span>
|
||||
<span className="typography-meta text-muted-foreground">
|
||||
{t('settings.openchamber.visual.field.macVibrancyHint')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{vibrancyChecked !== macVibrancyEnabled && (
|
||||
<div className="pl-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={vibrancyRestarting}
|
||||
onClick={() => {
|
||||
setVibrancyRestarting(true);
|
||||
void invokeDesktop('desktop_set_vibrancy', { enabled: vibrancyChecked });
|
||||
}}
|
||||
>
|
||||
{vibrancyRestarting
|
||||
? t('settings.openchamber.visual.actions.restarting')
|
||||
: t('settings.openchamber.visual.actions.saveAndRestart')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -201,6 +201,8 @@ type DesktopBridgeGlobal = {
|
||||
|
||||
type ElectronRuntimeGlobal = {
|
||||
runtime?: string;
|
||||
macVibrancy?: boolean;
|
||||
macVibrancySupported?: boolean;
|
||||
};
|
||||
|
||||
const getElectronRuntime = (): ElectronRuntimeGlobal | null => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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/에서 사용자 정의 테마를 가져옵니다',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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/ 导入自定义主题',
|
||||
|
||||
@@ -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/ 匯入自訂主題',
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user