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:
Bohdan Triapitsyn
2026-06-09 18:38:01 +03:00
parent f9acf68c8f
commit 3541bc63f9
19 changed files with 288 additions and 12 deletions
@@ -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>
)}