Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,19 @@ html[data-native-material="on"] .lightcode-sidebar-aside {
html[data-native-material="on"] .lightcode-sidebar-aside .lightcode-overlay-header {
background: transparent;
}
/* The floating overlay sidebar sits over dimmed content, where a translucent
tint reads muddy and hurts legibility — paint it fully opaque (the glass tint
at 100%). Docked sidebars keep the translucent tint set above. */
html[data-native-material="on"] .lightcode-sidebar-aside--overlay {
background: var(--content-background);
}
/* While the overlay sidebar slides out and snaps to the collapsed rail, the
spacer that reserves the rail's slot is briefly uncovered. Give it the same
glass tint as the docked collapsed rail so the strip never flashes raw
acrylic before the rail paints in. */
html[data-native-material="on"] .lightcode-sidebar-spacer {
background: var(--sidebar-glass-tint);
}
/* Windows-only frosting bump. DWM acrylic blurs whatever sits behind the window,
so the backdrop bleeds through the tint and drags the sidebar toward it: a dark
theme over a bright window washes out toward grey, while a light theme over a
Expand Down Expand Up @@ -1091,12 +1104,10 @@ html[data-sidebar-glass="on"]:not([data-native-material="on"])
.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);
}
/* In-app fallback overlay sidebar: opaque too — it floats over dimmed content,
so the faux-glass tint/blur used for the docked fallback would read muddy. */
html[data-sidebar-glass="on"]:not([data-native-material="on"]) .lightcode-sidebar-aside--overlay {
background: var(--sidebar-background);
}

/* Overlays (settings, git review, file editor, …) reuse the same AppShell, so
Expand Down Expand Up @@ -2405,6 +2416,27 @@ html[data-platform="darwin"] .lightcode-content-over-drag-region--drag {
}
}

/* Docked right/bottom panel content fade. The panel's <aside> background stays
opaque (so the OS blur never shows through); only this content layer fades.
Keyframes, not a transition, because the layer is clipped to zero by the
aside's collapsing size and a transition's start value is unreliable there. */
@keyframes lightcode-panel-content-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes lightcode-panel-content-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

.lightcode-image-lightbox__image {
max-width: calc(100vw - 6rem);
max-height: calc(100vh - 6rem);
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/views/MainView/parts/AppShell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ function ShellSidebarSpacer(props: { hasHeaders: boolean; forceSidebarExpanded:
if (!isOverlay) return null;
return (
<div
className={`shrink-0 ${!props.hasHeaders ? "-mt-5 h-[calc(100%+0.75rem)]" : ""}`}
className={`lightcode-sidebar-spacer shrink-0 ${!props.hasHeaders ? "-mt-5 h-[calc(100%+0.75rem)]" : ""}`}
style={{ width: SIDEBAR_COLLAPSED_WIDTH, minWidth: SIDEBAR_COLLAPSED_WIDTH }}
/>
);
Expand Down
63 changes: 47 additions & 16 deletions src/renderer/views/MainView/parts/AppShell/parts/AsideSlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export function AsideSlot(props: {
// Docked path: width/height animates open <-> closed.
const dockedDisplayWidth = !isHorizontal ? (isOpen ? targetWidth : 0) : undefined;
const dockedDisplayHeight = isHorizontal ? (isOpen ? targetHeight : 0) : undefined;
// Show: Faster fade in (300ms), fast width/height (150ms)
// Hide: Fast width/height (150ms), fast-ish fade out (200ms)
// Show: content fades in (300ms) over the opaque background, fast size (150ms).
// Hide: fast size (150ms), fast-ish content fade out (200ms).
// During an active drag, useResizablePanels writes transitionDuration: 0ms directly
// to the panel element so per-frame width/height updates aren't smoothed.
const dockedFadeDuration = isOpen ? "300ms" : "200ms";
Expand All @@ -49,31 +49,66 @@ export function AsideSlot(props: {
let asideClassName: string;
let asideStyle: CSSProperties;
if (overlay) {
asideClassName = `fixed bottom-0 right-0 z-50 flex flex-col overflow-hidden border-l border-[color:var(--border)] bg-[var(--content-background)] shadow-2xl transition-transform duration-300 will-change-transform ${
asideClassName = `fixed inset-y-0 right-0 z-50 flex flex-col overflow-hidden border-l border-[color:var(--border)] bg-[var(--content-background)] shadow-2xl transition-transform duration-300 will-change-transform ${
overlayReady ? "translate-x-0" : "translate-x-full"
}`;
asideStyle = {
top: overlayTop,
// Span the full window height so the opaque panel background reaches the
// very top — otherwise the strip above it shows the content-header row
// dimmed by the dialog backdrop, reading as a darker seam. The panel's own
// content is pushed below the OS titlebar/window-controls row via padding
// so its header buttons don't collide with the min/max/close controls.
paddingTop: overlayTop,
width: targetWidth,
minWidth: targetWidth,
};
} else {
// The background layer (this <aside>) stays fully opaque while the panel
// animates open, so the OS blur material (Windows acrylic / macOS vibrancy)
// never shows through. Only the size animates here; the panel *content*
// cross-fades on the inner layer below. Collapse the border to transparent
// while closed so the 0-size panel leaves no 1px hairline at the edge.
const borderColorClass = isOpen ? "border-[color:var(--border)]" : "border-transparent";
asideClassName = `relative overflow-hidden bg-[var(--content-background)] ${
isHorizontal
? "min-w-0 border-t border-[color:var(--border)]"
: "min-h-0 border-l border-[color:var(--border)]"
isHorizontal ? `min-w-0 border-t ${borderColorClass}` : `min-h-0 border-l ${borderColorClass}`
}`;
asideStyle = {
...(isHorizontal
? { height: dockedDisplayHeight, minHeight: dockedDisplayHeight }
: { width: dockedDisplayWidth, minWidth: dockedDisplayWidth }),
opacity: isOpen ? 1 : 0,
transitionProperty: "width, min-width, height, min-height, opacity, border-color",
transitionDuration: `${dockedSizeDuration}, ${dockedSizeDuration}, ${dockedSizeDuration}, ${dockedSizeDuration}, ${dockedFadeDuration}, 200ms`,
transitionProperty: "width, min-width, height, min-height, border-color",
transitionDuration: `${dockedSizeDuration}, ${dockedSizeDuration}, ${dockedSizeDuration}, ${dockedSizeDuration}, 200ms`,
transitionTimingFunction: isOpen ? "ease-out" : "ease-in",
willChange: "width, min-width, height, min-height, opacity",
willChange: "width, min-width, height, min-height",
};
}

// Content (header + body) cross-fades on the inner layer so the opaque <aside>
// background never reveals the OS blur material during the animation. The
// signal differs by mode: docked tracks isOpen (size animation); overlay
// tracks overlayReady so the content fades in step with the slide-in/out.
//
// Docked uses a keyframe because the layer is clipped to zero by the
// collapsing panel and a transition's start value is unreliable when revealed
// from a zero-clipped state (content would pop in at full opacity). The
// overlay is never clipped (it is a full-size panel that only translates), so
// a plain opacity transition is reliable there — and unlike the keyframe it
// fades cleanly on close from its current value instead of snapping to 0.
const contentVisible = overlay ? overlayReady : isOpen;
const contentFadeDuration = overlay ? "300ms" : dockedFadeDuration;
const contentFadeEase = contentVisible ? "ease-out" : "ease-in";
const innerStyle: CSSProperties = {
...(isHorizontal ? { height: targetHeight } : { width: targetWidth }),
opacity: contentVisible ? 1 : 0,
...(overlay
? { transition: `opacity ${contentFadeDuration} ${contentFadeEase}` }
: {
animation: `${
contentVisible ? "lightcode-panel-content-in" : "lightcode-panel-content-out"
} ${contentFadeDuration} ${contentFadeEase}`,
}),
willChange: "opacity",
};
const asideKey = overlay ? "overlay-aside" : "docked-aside";

return (
Expand All @@ -91,11 +126,7 @@ export function AsideSlot(props: {
/>
)}
<aside key={asideKey} ref={panelRef} className={asideClassName} style={asideStyle}>
<div
ref={panelInnerRef}
className="h-full w-full"
style={isHorizontal ? { height: targetHeight } : { width: targetWidth }}
>
<div ref={panelInnerRef} className="h-full w-full" style={innerStyle}>
{children}
</div>
</aside>
Expand Down
6 changes: 3 additions & 3 deletions src/shared/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ export const sharedSettingsSchema = z.object({
/** Show the projectless Home scope for OS-level agent sessions. */
homeScopeEnabled: z.boolean(),
/**
* Opt-in translucent ("liquid glass") sidebar. When on, the window uses a
* Translucent ("liquid glass") sidebar. When on, the window uses a
* native blur material where supported (macOS vibrancy, Windows 11 acrylic)
* and an in-app translucent fallback elsewhere. Default off.
* and an in-app translucent fallback elsewhere. Default on.
*/
sidebarTranslucency: z.boolean(),
/**
Expand Down Expand Up @@ -349,7 +349,7 @@ export const defaultSharedSettings: SharedSettings = {
threadRemoveAction: "archive",
newThreadMode: "page",
homeScopeEnabled: true,
sidebarTranslucency: false,
sidebarTranslucency: true,
sidebarGlassTint: { light: null, dark: null },
autoShowTerminalPanel: true,
gitReviewMode: "panel",
Expand Down