diff --git a/src/main/ipc/localHandlers.ts b/src/main/ipc/localHandlers.ts index c24f5378..98ed0db8 100644 --- a/src/main/ipc/localHandlers.ts +++ b/src/main/ipc/localHandlers.ts @@ -40,7 +40,9 @@ import { defineMainLocalIpcHandlers, type MainLocalIpcHandlerMap, type WindowChromePayload, + type WindowChromeResult, } from "@/shared/ipc"; +import { supportsNativeWindowMaterial, syncNativeThemeForMaterial } from "../window/windowMaterial"; import type { AgentInstanceConfig } from "@/shared/contracts"; import type { LightcodePaths } from "@/shared/lightcodePaths"; import { UsageLoginManager } from "../usageLogin/UsageLoginManager"; @@ -52,6 +54,8 @@ interface CreateLocalIpcHandlersOptions { updatePowerSaveBlocker(): void; autoUpdater: AutoUpdaterController; onSharedSettingsChanged?(): void; + /** Relaunch the app (exposed via the relaunchApp IPC). */ + requestRelaunch(): void; } function requireBrowserPanel(getter: () => BrowserPanelManager | null): BrowserPanelManager { @@ -132,6 +136,9 @@ export function createLocalIpcHandlers( } win.focus(); }, + relaunchApp: () => { + options.requestRelaunch(); + }, getHomeScopeLocation: () => process.platform === "win32" ? { kind: "windows", path: homedir() } @@ -191,10 +198,11 @@ export function createLocalIpcHandlers( options.onSharedSettingsChanged?.(); return instance; }, - setWindowChrome: async (payload: WindowChromePayload) => { + setWindowChrome: async (payload: WindowChromePayload): Promise => { + const nativeCapable = supportsNativeWindowMaterial(); const mainWindow = options.getMainWindow(); if (!mainWindow) { - return; + return { nativeCapable }; } if (process.platform === "win32" || process.platform === "linux") { mainWindow.setTitleBarOverlay({ @@ -203,6 +211,20 @@ export function createLocalIpcHandlers( height: 32, }); } + // Toggle the native translucency material live. macOS vibrancy is created + // with the window and revealed/hidden purely via CSS, so there is nothing + // to switch here. Windows acrylic is toggled at runtime (no relaunch). + const wantsMaterial = payload.materialEnabled === true && nativeCapable; + if (process.platform === "win32") { + mainWindow.setBackgroundMaterial(wantsMaterial ? "acrylic" : "none"); + mainWindow.setBackgroundColor( + wantsMaterial ? "#00000000" : payload.appearance === "dark" ? "#141416" : "#f1f1f4", + ); + } + if (wantsMaterial && payload.appearance) { + syncNativeThemeForMaterial(payload.appearance); + } + return { nativeCapable }; }, dbGetProjects: () => dbGetProjects(), dbGetThreads: () => dbGetThreads(), diff --git a/src/main/main.ts b/src/main/main.ts index 05770aa8..1637140f 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -93,19 +93,29 @@ function isCloseToTrayEnabled(): boolean { } /** - * Resolves the saved appearance so the native window opens with a matching - * background instead of flashing a fixed color before the renderer paints. + * Resolves the saved appearance + opt-in translucent ("liquid glass") sidebar in + * a single settings read, so the window opens already matching the theme and + * material (flash-free first paint) before the renderer paints. */ -function resolveAppAppearance(): "light" | "dark" { +function resolveWindowChromeOptions(): { + appearance: "light" | "dark"; + sidebarTranslucency: boolean; +} { let mode: "system" | "light" | "dark" = "dark"; + let wantGlass = false; if (lightcodePaths) { try { - mode = readSharedSettingsFile(lightcodePaths.settingsPath).themeMode; + const settings = readSharedSettingsFile(lightcodePaths.settingsPath); + mode = settings.themeMode; + wantGlass = settings.sidebarTranslucency === true; } catch { - // Fall back to dark. + // Fall back to dark / opaque. } } - return resolveThemeMode(mode, nativeTheme.shouldUseDarkColors); + return { + appearance: resolveThemeMode(mode, nativeTheme.shouldUseDarkColors), + sidebarTranslucency: wantGlass, + }; } function primeBrowserAllowFlags(): void { @@ -278,10 +288,16 @@ if (!hasSingleInstanceLock) { updatePowerSaveBlocker, autoUpdater: autoUpdaterController, onSharedSettingsChanged: primeBrowserAllowFlags, + requestRelaunch: () => { + isQuitting = true; + app.relaunch(); + app.quit(); + }, }), callSupervisor: (name, payload) => supervisorClient.call(name, payload), }); + const windowChrome = resolveWindowChromeOptions(); mainWindow = createMainWindow({ title: getAppName(channel, isDev), isDev, @@ -296,7 +312,8 @@ if (!hasSingleInstanceLock) { sentryEnabled, windowChromeHeight: WINDOW_CHROME_HEIGHT, browserUserAgent: chromeLikeUserAgent, - appearance: resolveAppAppearance(), + appearance: windowChrome.appearance, + sidebarTranslucency: windowChrome.sidebarTranslucency, ...(process.env.VITE_DEV_SERVER_URL ? { devServerUrl: process.env.VITE_DEV_SERVER_URL } : {}), onClosed: () => { mainWindow = null; @@ -371,6 +388,7 @@ if (!hasSingleInstanceLock) { return; } if (BrowserWindow.getAllWindows().length === 0) { + const reopenChrome = resolveWindowChromeOptions(); mainWindow = createMainWindow({ title: getAppName(channel, isDev), isDev, @@ -385,7 +403,8 @@ if (!hasSingleInstanceLock) { sentryEnabled, windowChromeHeight: WINDOW_CHROME_HEIGHT, browserUserAgent: chromeLikeUserAgent, - appearance: resolveAppAppearance(), + appearance: reopenChrome.appearance, + sidebarTranslucency: reopenChrome.sidebarTranslucency, ...(process.env.VITE_DEV_SERVER_URL ? { devServerUrl: process.env.VITE_DEV_SERVER_URL } : {}), diff --git a/src/main/sharedSettingsFile.test.ts b/src/main/sharedSettingsFile.test.ts index 9ea6d58f..89836320 100644 --- a/src/main/sharedSettingsFile.test.ts +++ b/src/main/sharedSettingsFile.test.ts @@ -75,6 +75,7 @@ describe("sharedSettingsFile", () => { threadRemoveAction: "archive", newThreadMode: "page", homeScopeEnabled: true, + sidebarTranslucency: false, autoShowTerminalPanel: true, gitReviewMode: "panel", prCreateMode: "dialog", @@ -162,6 +163,7 @@ describe("sharedSettingsFile", () => { threadRemoveAction: "archive", newThreadMode: "page", homeScopeEnabled: true, + sidebarTranslucency: false, autoShowTerminalPanel: true, gitReviewMode: "panel", prCreateMode: "dialog", diff --git a/src/main/window/createMainWindow.test.ts b/src/main/window/createMainWindow.test.ts index 02d3f80b..7849967c 100644 --- a/src/main/window/createMainWindow.test.ts +++ b/src/main/window/createMainWindow.test.ts @@ -85,6 +85,7 @@ describe("createMainWindow", () => { windowChromeHeight: 32, browserUserAgent: userAgent, appearance: "dark", + sidebarTranslucency: false, onClosed: vi.fn<() => void>(), }); diff --git a/src/main/window/createMainWindow.ts b/src/main/window/createMainWindow.ts index 3e6fc81b..0b395c13 100644 --- a/src/main/window/createMainWindow.ts +++ b/src/main/window/createMainWindow.ts @@ -2,6 +2,7 @@ import { dbGetState, dbSetState } from "../db"; import { BrowserWindow, screen, type RenderProcessGoneDetails } from "electron"; import type { LightcodeChannel } from "@/shared/channel"; import { installSessionPermissions } from "../browser/permissions"; +import { supportsNativeWindowMaterial, syncNativeThemeForMaterial } from "./windowMaterial"; interface WindowBounds { x?: number; @@ -63,6 +64,8 @@ export interface CreateMainWindowOptions { browserUserAgent: string; /** Saved appearance, so the native window opens matching the theme. */ appearance: "light" | "dark"; + /** Saved opt-in translucent ("liquid glass") sidebar, so the window opens with the material already applied. */ + sidebarTranslucency: boolean; onClosed(): void; onClose?: (event: Electron.Event) => void; onRendererProcessGone?: (details: RenderProcessGoneDetails) => void; @@ -77,6 +80,20 @@ export function createMainWindow(options: CreateMainWindowOptions): BrowserWindo // setWindowChrome values, so the first frame doesn't flash a fixed palette. const backgroundColor = isDark ? "#141416" : "#f1f1f4"; const symbolColor = isDark ? "#fafafa" : "#1f2937"; + // macOS: always create the window transparent + vibrancy-capable so the glass + // sidebar can be toggled live (the renderer reveals/hides it purely via CSS — + // with glass off the opaque content simply covers the material). macOS can't + // turn an opaque window transparent at runtime, so the capability has to exist + // from creation. Windows acrylic is applied here for a flash-free first paint + // when glass is already on, and toggled live via setBackgroundMaterial. + const isMacOS = process.platform === "darwin"; + const winGlassAtStart = + process.platform === "win32" && options.sidebarTranslucency && supportsNativeWindowMaterial(); + if (options.sidebarTranslucency && supportsNativeWindowMaterial()) { + // Match the native appearance to the app theme so the material renders in the + // right light/dark variant from the first frame. + syncNativeThemeForMaterial(options.appearance); + } const window = new BrowserWindow({ title: options.title, show: false, @@ -85,8 +102,12 @@ export function createMainWindow(options: CreateMainWindowOptions): BrowserWindo ...(saved?.x != null && saved?.y != null ? { x: saved.x, y: saved.y } : {}), minWidth: 540, minHeight: 720, - backgroundColor, + backgroundColor: isMacOS || winGlassAtStart ? "#00000000" : backgroundColor, autoHideMenuBar: true, + ...(isMacOS + ? { vibrancy: "sidebar" as const, visualEffectState: "active" as const, transparent: true } + : {}), + ...(winGlassAtStart ? { backgroundMaterial: "acrylic" as const } : {}), ...(supportsTitleBarOverlay ? { titleBarStyle: "hidden" as const, diff --git a/src/main/window/windowMaterial.ts b/src/main/window/windowMaterial.ts new file mode 100644 index 00000000..ed1d3050 --- /dev/null +++ b/src/main/window/windowMaterial.ts @@ -0,0 +1,44 @@ +import { nativeTheme } from "electron"; +import { release } from "node:os"; + +/** + * Native "liquid glass" window materials. + * + * The opt-in translucent sidebar relies on an OS-composited blur behind the + * window (macOS `NSVisualEffectView` vibrancy / Windows 11 DWM acrylic). On + * macOS these can only be revealed when the window is *created* transparent — + * an opaque window cannot be made transparent at runtime — so the material is + * applied once in {@link createMainWindow} and toggling the setting requires a + * relaunch. This module centralizes the OS capability check and native-theme sync. + * + * macOS 26 "Liquid Glass" (`NSGlassEffectView`) is not exposed by Electron, so + * the closest officially-supported material is `vibrancy: "sidebar"`, which the + * OS already re-skins toward the Tahoe look. + */ + +/** + * Windows 11 22H2 (build 22621) is the first build with a stable DWM acrylic + * system backdrop; earlier Windows builds and Windows 10 have no usable native + * blur, so they fall back to the in-app CSS imitation. + */ +function isWindows11AcrylicCapable(): boolean { + if (process.platform !== "win32") { + return false; + } + const build = Number(release().split(".")[2] ?? "0"); + return Number.isFinite(build) && build >= 22621; +} + +/** Whether the current OS can render a native blur material behind the window. */ +export function supportsNativeWindowMaterial(): boolean { + return process.platform === "darwin" || isWindows11AcrylicCapable(); +} + +/** + * Mirrors the app appearance onto the native theme so an active vibrancy/acrylic + * material renders in the matching light/dark variant. Without this it follows + * the OS appearance (e.g. a light app over a dark OS shows a dark frosted sidebar). + */ +export function syncNativeThemeForMaterial(appearance: "light" | "dark"): void { + nativeTheme.themeSource = appearance; +} diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 052bd08b..bcd44386 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -263,7 +263,7 @@ export function App() { `[renderer] +${Date.now() - loadT0}ms: rendering spinner (hydrated=${storeHydrated})`, ); return ( - +
@@ -275,7 +275,7 @@ export function App() { } return ( - + diff --git a/src/renderer/components/layout/OverlayShell.tsx b/src/renderer/components/layout/OverlayShell.tsx index 137704cb..6a3131b5 100644 --- a/src/renderer/components/layout/OverlayShell.tsx +++ b/src/renderer/components/layout/OverlayShell.tsx @@ -65,6 +65,11 @@ export function OverlayShell(props: { return (
("dark"); const toastContentClassName = "min-w-0 p-0 pr-1"; const toastDescriptionClassName = @@ -61,14 +69,22 @@ function ToastAction({ actionProps, actionLabel, isCopyAction }: ToastActionProp ); } -export function AppProvider(props: { children: ReactNode }) { - const { children } = props; +export function AppProvider(props: { children: ReactNode; contentReady?: boolean }) { + // `contentReady` gates the glass material: the window stays opaque through + // loading and only goes translucent once the main content is mounted, so the + // app never shows a bare translucent window mid-load. + const { children, contentReady = false } = props; const themeMode = useSharedSettings((state) => state.themeMode); const themePreset = useSharedSettings((state) => state.themePreset); const [prefersDark, setPrefersDark] = useState(systemPrefersDark); const syncSystemPreference = useEffectEvent((matches: boolean) => { setPrefersDark(matches); }); + const sidebarTranslucency = useSharedSettings((state) => state.sidebarTranslucency); + const [reducedTransparency, setReducedTransparency] = useState(systemPrefersReducedTransparency); + const syncReducedTransparency = useEffectEvent((matches: boolean) => { + setReducedTransparency(matches); + }); useEffect(() => { if (typeof window === "undefined" || typeof window.matchMedia !== "function") { @@ -87,7 +103,26 @@ export function AppProvider(props: { children: ReactNode }) { }; }, []); + useEffect(() => { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") { + return; + } + + const media = window.matchMedia("(prefers-reduced-transparency: reduce)"); + const onChange = (event: MediaQueryListEvent) => { + syncReducedTransparency(event.matches); + }; + + syncReducedTransparency(media.matches); + media.addEventListener("change", onChange); + return () => { + media.removeEventListener("change", onChange); + }; + }, []); + const appearance = resolveThemeMode(themeMode, prefersDark); + // The opt-in translucent sidebar, suppressed when the OS asks for reduced transparency. + const glassEnabled = sidebarTranslucency && !reducedTransparency; useEffect(() => { const root = document.documentElement; @@ -98,6 +133,12 @@ export function AppProvider(props: { children: ReactNode }) { persistThemeBoot(appearance, themePreset); }, [appearance, themePreset]); + // Gates the in-app CSS sidebar tint/fallback. Held off until content is ready + // so the loading screen stays opaque. + useEffect(() => { + document.documentElement.dataset.sidebarGlass = glassEnabled && contentReady ? "on" : "off"; + }, [glassEnabled, contentReady]); + useEffect(() => { if (typeof window === "undefined" || !("lightcode" in window)) { return; @@ -105,18 +146,27 @@ export function AppProvider(props: { children: ReactNode }) { const root = document.documentElement; const styles = window.getComputedStyle(root); + const wantMaterial = glassEnabled && contentReady; void readBridge() .setWindowChrome({ backgroundColor: styles.getPropertyValue("--window-overlay-background").trim() || "rgba(0, 0, 0, 0)", symbolColor: appearance === "dark" ? "#fafafa" : "#1f2937", + materialEnabled: wantMaterial, + appearance, + }) + .then((result) => { + // The native material is toggled live by the main process; reveal it via + // the transparent-window CSS only where the OS actually supports it. + root.dataset.nativeMaterial = wantMaterial && !!result?.nativeCapable ? "on" : "off"; }) .catch((error: unknown) => { + root.dataset.nativeMaterial = "off"; captureRendererException(error, { featureArea: "window-chrome" }); // Keep renderer boot resilient if Electron rejects a color value. }); - }, [appearance]); + }, [appearance, glassEnabled, contentReady]); return ( diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index 84315efe..b68e0b03 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -37,6 +37,10 @@ initializeRendererSentry(); document.documentElement.dataset.platform = typeof window !== "undefined" && "lightcode" in window ? readBridge().platform : "unknown"; +// The translucent ("liquid glass") sidebar is applied by provider.tsx only once +// the main content is ready — the window stays opaque (the index.html boot +// background) through loading so it doesn't show a bare translucent window. + // Apply the cached appearance + theme before first paint so a non-default theme // doesn't flash the base palette on launch. bootstrapAppThemeFromCache(); diff --git a/src/renderer/state/sharedSettingsStore.ts b/src/renderer/state/sharedSettingsStore.ts index 10c61033..7c394edd 100644 --- a/src/renderer/state/sharedSettingsStore.ts +++ b/src/renderer/state/sharedSettingsStore.ts @@ -55,6 +55,7 @@ interface SharedSettingsState extends SharedSettings { setThreadRemoveAction: (value: ThreadRemoveAction) => void; setNewThreadMode: (value: NewThreadMode) => void; setHomeScopeEnabled: (value: boolean) => void; + setSidebarTranslucency: (value: boolean) => void; setAutoShowTerminalPanel: (value: boolean) => void; setGitReviewMode: (value: GitReviewMode) => void; setPrCreateMode: (value: PrCreateMode) => void; @@ -313,6 +314,11 @@ export const useSharedSettings = create()((set, get) => ({ set({ homeScopeEnabled }); persistSettings(selectSharedSettings(get())); }, + setSidebarTranslucency: (sidebarTranslucency) => { + if (get().sidebarTranslucency === sidebarTranslucency) return; + set({ sidebarTranslucency }); + persistSettings(selectSharedSettings(get())); + }, setAutoShowTerminalPanel: (autoShowTerminalPanel) => { set({ autoShowTerminalPanel }); persistSettings(selectSharedSettings(get())); @@ -568,6 +574,7 @@ function selectSharedSettings(state: SharedSettingsState): SharedSettingsInput { threadRemoveAction: state.threadRemoveAction, newThreadMode: state.newThreadMode, homeScopeEnabled: state.homeScopeEnabled, + sidebarTranslucency: state.sidebarTranslucency, autoShowTerminalPanel: state.autoShowTerminalPanel, gitReviewMode: state.gitReviewMode, prCreateMode: state.prCreateMode, diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 2b10c1ee..823c7cfd 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -788,6 +788,12 @@ --pr-merged: oklch(0.7 0.17 292); --git-branch-tone: var(--muted); --sidebar-background: var(--surface); + /* Overlay painted on the translucent ("liquid glass") sidebar: the content + background (matching the main panes) at partial alpha. The OS blur behind + is the *wallpaper*, not the theme color, so higher alpha keeps the sidebar + reading as the theme (less wallpaper/grey showing); lower alpha shows more + wallpaper blur (more glassy, but tinted by whatever's on the desktop). */ + --sidebar-glass-tint: color-mix(in oklab, var(--content-background) 35%, transparent); --content-background: var(--background); --window-header-background: var(--sidebar-background); --window-overlay-background: rgba(0, 0, 0, 0); @@ -871,6 +877,9 @@ /* App-specific */ --sidebar-background: oklch(0.22 0.004 286); --content-background: oklch(0.2 0.004 286); + /* Dark surfaces read muddier through heavy blur, so lean more on the theme + color (higher alpha = less transparent) than the light default above. */ + --sidebar-glass-tint: color-mix(in oklab, var(--content-background) 65%, transparent); --window-header-background: var(--sidebar-background); --composer-surface: oklch(0.25 0.004 286); } @@ -1008,6 +1017,122 @@ body { z-index: 20; } +/* === Translucent ("liquid glass") sidebar =================================== + Opt-in via the Appearance toggle (default off). Two paths: + + 1. Native material — [data-native-material="on"] is set by the renderer ONLY + after the main process confirms it actually applied a blur material + (macOS vibrancy / Windows 11 acrylic). The whole window is blurred by the + OS, so we clear the opaque window layers (body, shell) and re-cover the + main content with an opaque pane; only the sidebar (and the thin titlebar / + resize-handle gaps) stay translucent.
has no background of its own, + so it must be given one explicitly or the whole window goes translucent. + + 2. In-app fallback — [data-sidebar-glass="on"] without a native material + (Linux, Windows 10, older builds). The window stays opaque; the docked, + flush-left sidebar gets a faux-translucent gradient + bevel (a real + backdrop-filter would blur almost nothing there). Real blur is reserved + for the floating *overlay* sidebar, which genuinely sits over content. + + These rules are intentionally unlayered so they win over Tailwind utilities + (e.g. the sidebar's bg-[var(--content-background)] header class). */ + +/* 1. Native material: clear opaque window layers, keep main content opaque. + index.html's boot script sets an inline opaque background on to avoid a + load flash; override it with !important so the OS blur shows through. */ +html[data-native-material="on"] { + background: transparent !important; +} +html[data-native-material="on"] body, +html[data-native-material="on"] .lightcode-shell { + background: transparent; +} +html[data-native-material="on"] .lightcode-shell main { + background: var(--content-background); +} +/* Faint tint so sidebar text/icons stay legible over the live OS blur. The + sidebar header (shared .lightcode-overlay-header class) is forced transparent + so the aside's single tint layer shows through instead of stacking. */ +html[data-native-material="on"] .lightcode-sidebar-aside { + background: var(--sidebar-glass-tint); +} +html[data-native-material="on"] .lightcode-sidebar-aside .lightcode-overlay-header { + background: transparent; +} + +/* 2. In-app fallback: opaque window, faux-translucent gradient sidebar. */ +html[data-sidebar-glass="on"]:not([data-native-material="on"]) .lightcode-sidebar-aside { + background: linear-gradient( + 180deg, + color-mix(in oklab, var(--sidebar-background) 90%, transparent) 0%, + color-mix(in oklab, var(--sidebar-background) 74%, transparent) 100% + ); + box-shadow: + inset 1px 0 0 color-mix(in oklab, var(--foreground) 7%, transparent), + inset 0 1px 0 color-mix(in oklab, var(--foreground) 9%, transparent); +} +html[data-sidebar-glass="on"]:not([data-native-material="on"]) + .lightcode-sidebar-aside + .lightcode-overlay-header { + background: transparent; +} +/* Real blur only for the floating overlay sidebar (it sits over content). */ +@supports (backdrop-filter: blur(1px)) { + html[data-sidebar-glass="on"]:not([data-native-material="on"]) .lightcode-sidebar-aside--overlay { + background: color-mix(in oklab, var(--sidebar-background) 70%, transparent); + backdrop-filter: blur(18px) saturate(1.5); + } +} + +/* Overlays (settings, git review, file editor, …) reuse the same AppShell, so + they get the glass sidebar too. The overlay sidebar body normally paints an + opaque surface (`overlaySidebarSurfaceClass`); clear it inside the sidebar + column so the aside's single glass tint shows through and the header (already + transparent) matches — no seam. Docked tool panels use the same surface class + but live outside `.lightcode-sidebar-aside`, so they stay opaque. */ +html[data-sidebar-glass="on"] .lightcode-sidebar-aside .lightcode-overlay-surface { + background: transparent; +} +/* For overlays that actually have a glass sidebar, let the OverlayShell cover + pass the native blur through (its opaque
still covers the content). + Plain dialogs without a sidebar keep their opaque backdrop. */ +html[data-native-material="on"] [data-overlay-surface]:has(.lightcode-sidebar-aside) { + background: transparent; +} +/* …and hide the base app behind such an overlay so the overlay's translucent + sidebar reveals the desktop, not the app underneath (which would bleed through + the tint). visibility:hidden keeps the app mounted — preserving scroll, chat, + and terminal state — while painting nothing. Hide every shell, then keep the + overlay's own shell visible. Gated on [data-overlay-visible] (set only while + the overlay is fully shown) so on open/close the base app fades in/out with + the overlay instead of flashing bare desktop underneath. */ +html[data-native-material="on"] + #root:has([data-overlay-surface][data-overlay-visible] .lightcode-sidebar-aside) + .lightcode-shell { + visibility: hidden; +} +html[data-native-material="on"] + #root:has([data-overlay-surface][data-overlay-visible] .lightcode-sidebar-aside) + [data-overlay-surface] + .lightcode-shell { + visibility: visible; +} + +/* Accessibility: drop translucency when the OS asks for reduced transparency. + The renderer also gates this in JS, so this is a first-paint safety net. */ +@media (prefers-reduced-transparency: reduce) { + html[data-native-material="on"] body, + html[data-native-material="on"] .lightcode-shell { + background: var(--content-background); + } + html[data-sidebar-glass="on"] .lightcode-sidebar-aside:not(.lightcode-sidebar-aside--overlay), + html[data-native-material="on"] .lightcode-sidebar-aside:not(.lightcode-sidebar-aside--overlay) { + background: transparent; + backdrop-filter: none; + box-shadow: none; + } +} + /* Counterpart to .lightcode-content-over-drag-region: used on pane headers when the pane can't be reordered (single-pane layouts), so the header acts as the window drag region instead of a dnd handle. macOS-only — diff --git a/src/renderer/views/MainView/parts/AppShell/AppShell.tsx b/src/renderer/views/MainView/parts/AppShell/AppShell.tsx index 02d5bcbe..e13a65d9 100644 --- a/src/renderer/views/MainView/parts/AppShell/AppShell.tsx +++ b/src/renderer/views/MainView/parts/AppShell/AppShell.tsx @@ -254,9 +254,9 @@ function ShellSidebarAside(props: { return (