From 2c30af5807292c57f7dfb975e037f348829d181a Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 10 Aug 2026 14:32:42 +0300 Subject: [PATCH] refactor: remove macOS vibrancy support and unify glass surfaces Drops the macOS vibrancy toggle, IPC, and related settings copy Updates dialogs, popovers, tooltips, and dropdowns to use shared glass styles Adds sticky header fade behavior to model picker and sidebar lists --- packages/electron/main.mjs | 79 +------- packages/electron/preload.mjs | 22 --- .../chat/work-status/DOCUMENTATION.md | 2 + .../chat/work-status/WorkStatusPanel.tsx | 5 +- packages/ui/src/components/layout/Header.tsx | 2 +- packages/ui/src/components/layout/Sidebar.tsx | 2 +- .../layout/TitlebarLeftControls.tsx | 5 - .../model-picker/ModelPickerList.tsx | 172 ++++++++++++++++-- .../openchamber/OpenChamberVisualSettings.tsx | 41 ----- .../session/sidebar/SessionNodeItem.tsx | 1 - .../session/sidebar/SidebarProjectsList.tsx | 2 +- .../ui/src/components/ui/context-menu.tsx | 1 - packages/ui/src/components/ui/dialog.tsx | 4 +- .../src/components/ui/dropdown-menu.styles.ts | 2 +- .../ui/src/components/ui/dropdown-menu.tsx | 2 - packages/ui/src/components/ui/select.tsx | 3 +- packages/ui/src/components/ui/tooltip.tsx | 2 +- .../src/components/views/SettingsWindow.tsx | 4 +- .../ui/src/contexts/ThemeSystemContext.tsx | 4 +- packages/ui/src/index.css | 63 +------ packages/ui/src/lib/desktop.ts | 2 - .../ui/src/lib/i18n/messages/de.settings.ts | 2 - .../ui/src/lib/i18n/messages/en.settings.ts | 2 - .../ui/src/lib/i18n/messages/es.settings.ts | 2 - .../ui/src/lib/i18n/messages/fr.settings.ts | 2 - .../ui/src/lib/i18n/messages/ja.settings.ts | 2 - .../ui/src/lib/i18n/messages/ko.settings.ts | 2 - .../ui/src/lib/i18n/messages/pl.settings.ts | 2 - .../src/lib/i18n/messages/pt-BR.settings.ts | 2 - .../ui/src/lib/i18n/messages/uk.settings.ts | 2 - .../src/lib/i18n/messages/zh-CN.settings.ts | 2 - .../src/lib/i18n/messages/zh-TW.settings.ts | 2 - packages/ui/src/lib/settings/search.ts | 8 - packages/ui/src/lib/theme/cssGenerator.ts | 21 --- packages/ui/src/styles/design-system.css | 102 ++++++++++- packages/ui/src/types/desktop.d.ts | 2 +- 36 files changed, 273 insertions(+), 302 deletions(-) diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 0467a165..d5be40ad 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -1900,36 +1900,6 @@ 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()) { @@ -2359,8 +2329,6 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } const desktopMacosMajor = String(macosMajorVersion()); const usesFramelessChrome = process.platform === 'win32' || process.platform === 'linux'; const usesCustomTitleBar = process.platform === 'darwin' || usesFramelessChrome; - // macOS vibrancy, on by default; users can disable it (Appearance settings). - const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false; const trayEnabled = process.platform !== 'darwin' || readSettingsRoot().desktopMacMenuBarEnabled !== false; const titleBarOverlayEnabled = false; const autoHidesNativeMenuBar = process.platform !== 'darwin'; @@ -2376,11 +2344,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } minHeight: MIN_WINDOW_HEIGHT, icon: windowIconPath, show: false, - 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. + backgroundColor: '#151313', frame: usesFramelessChrome ? false : undefined, autoHideMenuBar: autoHidesNativeMenuBar, // Electron's hiddenInset adds its own extra inset, which leaves the controls @@ -2396,7 +2360,6 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } `--openchamber-runtime-headers=${JSON.stringify(desktopRequestHeaders)}`, `--openchamber-home=${desktopHome}`, `--openchamber-macos-major=${desktopMacosMajor}`, - `--openchamber-mac-vibrancy=${useVibrancy ? '1' : '0'}`, `--openchamber-tray-enabled=${trayEnabled ? '1' : '0'}`, `--openchamber-boot-outcome=${JSON.stringify(state.bootOutcome || null)}`, `--openchamber-relay-host-id=${rendererRuntimeConfig.relayHostId || ''}`, @@ -2447,18 +2410,11 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } }; 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); } @@ -2613,7 +2569,6 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } } browserWindow.show(); browserWindow.focus(); - if (useVibrancy) applyMacVibrancy(browserWindow); }); if (url) { @@ -2777,8 +2732,6 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj const desktopHome = os.homedir() || ''; const desktopMacosMajor = String(macosMajorVersion()); const usesFramelessChrome = process.platform === 'win32' || process.platform === 'linux'; - // macOS vibrancy, on by default; users can disable it (Appearance settings). - const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false; const trayEnabled = process.platform !== 'darwin' || readSettingsRoot().desktopMacMenuBarEnabled !== false; const browserWindow = new BrowserWindow({ title: 'OpenChamber Mini Chat', @@ -2788,11 +2741,7 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj minHeight: MINI_CHAT_MIN_WINDOW_HEIGHT, icon: getWindowIconPath(), show: false, - 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. + backgroundColor: '#151313', frame: usesFramelessChrome ? false : undefined, autoHideMenuBar: process.platform !== 'darwin', titleBarStyle: process.platform === 'darwin' || usesFramelessChrome ? 'hidden' : 'default', @@ -2844,17 +2793,13 @@ 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 }) => { @@ -4205,26 +4150,6 @@ const handleInvoke = async (browserWindow, command, args = {}) => { return null; } - case 'desktop_set_vibrancy': { - // 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 = enabled; - }); - 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': { assertUpdaterCapability({ packaged: app.isPackaged }); const currentVersion = APP_VERSION; diff --git a/packages/electron/preload.mjs b/packages/electron/preload.mjs index 33a9a6c9..f3f96667 100644 --- a/packages/electron/preload.mjs +++ b/packages/electron/preload.mjs @@ -18,10 +18,6 @@ const runtimeHeadersRaw = readArgValue('--openchamber-runtime-headers'); 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'; const trayEnabled = process.platform !== 'darwin' || readArgValue('--openchamber-tray-enabled') !== '0'; // Preload re-executes on every cross-origin navigation (we run with @@ -97,8 +93,6 @@ if (Number.isFinite(macosMajor) && macosMajor > 0) { contextBridge.exposeInMainWorld('__OPENCHAMBER_ELECTRON__', { runtime: 'electron', arch: process.arch, - macVibrancy: hasMacVibrancy, - macVibrancySupported, trayEnabled, }); @@ -148,18 +142,6 @@ 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. @@ -173,10 +155,6 @@ 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/chat/work-status/DOCUMENTATION.md b/packages/ui/src/components/chat/work-status/DOCUMENTATION.md index 9a9cad2e..44e96381 100644 --- a/packages/ui/src/components/chat/work-status/DOCUMENTATION.md +++ b/packages/ui/src/components/chat/work-status/DOCUMENTATION.md @@ -25,6 +25,8 @@ It is **not** a context-panel surface. It is not registered in `lib/surfaces/registry.ts`, has no rail icon, no tab, no persisted width and no resizer. It is a card floating inside the chat column — rounded border, faint fill, its own margin — rather than a docked pane flush against the window edge. +When it overlays the transcript, it uses the shared `oc-glass-panel` surface; +the inline card keeps its lighter, non-blurred fill instead. ## Placement diff --git a/packages/ui/src/components/chat/work-status/WorkStatusPanel.tsx b/packages/ui/src/components/chat/work-status/WorkStatusPanel.tsx index 825c2864..5315a867 100644 --- a/packages/ui/src/components/chat/work-status/WorkStatusPanel.tsx +++ b/packages/ui/src/components/chat/work-status/WorkStatusPanel.tsx @@ -178,13 +178,14 @@ export const WorkStatusPanel: React.FC = ({ sessionId, directory, visible // Beside the transcript the translucent fill reads as depth; on top // of it, message bubbles showed straight through the rows. Frosting // separates the two without going fully opaque. - 'bg-[var(--surface-muted)]/80 backdrop-blur-md', + 'oc-glass-panel', ], // An empty card is a border around a settings icon, which reads as a // fault rather than as "nothing to report". renderedSections === 0 && 'border-transparent bg-transparent shadow-none', 'motion-reduce:transition-none', - 'rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-muted)]/40', + 'rounded-xl border border-[var(--interactive-border)]', + !overlay && 'bg-[var(--surface-muted)]/40', // A lighter version of the composer's lift: the same shape, but this // card is taller, so the composer's spread reads as heavy here. 'shadow-[0_2px_8px_-3px_rgb(0_0_0_/_0.08)]', diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 03e4c82e..e854e56e 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -336,7 +336,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({ {isDesktopApp ? (
diff --git a/packages/ui/src/components/layout/Sidebar.tsx b/packages/ui/src/components/layout/Sidebar.tsx index 92b7183b..a716541b 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 will-change-[width] motion-reduce:transition-none', - 'bg-sidebar oc-vibrancy-surface', + 'bg-sidebar', !isOpen && 'border-r-0', className, )} diff --git a/packages/ui/src/components/layout/TitlebarLeftControls.tsx b/packages/ui/src/components/layout/TitlebarLeftControls.tsx index 01f4e34f..c8484eb7 100644 --- a/packages/ui/src/components/layout/TitlebarLeftControls.tsx +++ b/packages/ui/src/components/layout/TitlebarLeftControls.tsx @@ -28,7 +28,6 @@ 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); @@ -129,10 +128,6 @@ export const TitlebarLeftControls: React.FC = () => { ) : null}
diff --git a/packages/ui/src/components/model-picker/ModelPickerList.tsx b/packages/ui/src/components/model-picker/ModelPickerList.tsx index 99080f9c..3b778c36 100644 --- a/packages/ui/src/components/model-picker/ModelPickerList.tsx +++ b/packages/ui/src/components/model-picker/ModelPickerList.tsx @@ -288,6 +288,9 @@ const SortableProviderSection: React.FC<{ }; const STICKY_HEADER_OFFSET = 32; +const STICKY_FADE_MAX_SIZE = 48; +const STICKY_FADE_MIN_SIZE = 32; +const STICKY_FADE_CLEAR_MAX_SIZE = 24; const scrollIntoView = (container: HTMLElement | null, node: HTMLElement | null) => { if (!node) return; @@ -417,6 +420,9 @@ export const ModelPickerList: React.FC = ({ const selectionStore = selectionStoreRef.current; const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); const scrollRef = React.useRef(null); + const sectionHeaderSentinelRefs = React.useRef>(new Map()); + const stickyFadeSizeRef = React.useRef(0); + const [stuckSectionHeaders, setStuckSectionHeaders] = React.useState>(new Set()); const keyboardOwnsSelectionRef = React.useRef(false); const lastMousePositionRef = React.useRef<{ x: number; y: number } | null>(null); const collapsedRecord = useModelPickerSectionsStore((state) => state.collapsedSections); @@ -495,6 +501,70 @@ export const ModelPickerList: React.FC = ({ }) .filter((provider) => provider.models.length > 0), [allowedProviderSet, isHidden, isModelAllowed, matchesQuery, orderedProviders]); + const visibleSectionKeys = React.useMemo(() => [ + ...(filteredFavorites.length > 0 ? ['favorites'] : []), + ...(filteredRecents.length > 0 ? ['recent'] : []), + ...filteredProviders.map((provider) => `provider:${provider.id}`), + ], [filteredFavorites.length, filteredProviders, filteredRecents.length]); + + React.useEffect(() => { + if (!stickyHeaders || !scrollRef.current) { + setStuckSectionHeaders((previous) => previous.size === 0 ? previous : new Set()); + return; + } + + const root = scrollRef.current; + const observer = new IntersectionObserver((entries) => { + setStuckSectionHeaders((previous) => { + const next = new Set(previous); + let changed = false; + for (const entry of entries) { + const sectionKey = (entry.target as HTMLElement).dataset.modelSectionKey; + if (!sectionKey) continue; + const rootTop = entry.rootBounds?.top ?? root.getBoundingClientRect().top; + const isAboveScroller = !entry.isIntersecting && entry.boundingClientRect.top < rootTop; + if (next.has(sectionKey) === isAboveScroller) continue; + changed = true; + if (isAboveScroller) next.add(sectionKey); + else next.delete(sectionKey); + } + return changed ? next : previous; + }); + }, { root, threshold: 0 }); + + sectionHeaderSentinelRefs.current.forEach((element) => { + if (element) observer.observe(element); + }); + return () => observer.disconnect(); + }, [stickyHeaders, visibleSectionKeys]); + + const syncStickyFade = React.useCallback((scroller: HTMLElement) => { + const hasTopScroll = scroller.scrollTop > 1; + const fadeSize = hasTopScroll + ? Math.min(STICKY_FADE_MIN_SIZE + scroller.scrollTop, STICKY_FADE_MAX_SIZE) + : 0; + stickyFadeSizeRef.current = fadeSize; + scroller.style.setProperty('--scroll-shadow-top-size', `${fadeSize}px`); + scroller.style.setProperty( + '--scroll-shadow-top-clear-size', + `${Math.min(Math.max(fadeSize - 8, 0), STICKY_FADE_CLEAR_MAX_SIZE)}px`, + ); + }, []); + + const blockStickyFadeInteraction = React.useCallback(( + event: React.MouseEvent | React.PointerEvent, + ) => { + if ((event.target as Element).closest('[data-overlay-scrollbar-thumb], [data-model-picker-sticky-header]')) return; + const eventY = event.clientY - event.currentTarget.getBoundingClientRect().top; + if (eventY >= stickyFadeSizeRef.current) return; + event.preventDefault(); + event.stopPropagation(); + }, []); + + React.useLayoutEffect(() => { + if (stickyHeaders && scrollRef.current) syncStickyFade(scrollRef.current); + }, [stickyHeaders, syncStickyFade, visibleSectionKeys]); + const flatModelList = React.useMemo(() => { const items: ModelPickerEntry[] = []; if (!collapsedSections.has('favorites')) filteredFavorites.forEach((entry) => items.push(entry)); @@ -571,7 +641,7 @@ export const ModelPickerList: React.FC = ({ const headerClassName = cn( 'typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 px-2 py-1.5', - stickyHeaders && 'sticky top-0 z-10 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]', + stickyHeaders && 'sticky top-0 z-20', sectionHeaderClassName, ); @@ -678,6 +748,15 @@ export const ModelPickerList: React.FC = ({ const isSectionCollapsed = (key: string) => collapsedSections.has(key); const toggleSectionCollapsed = (key: string) => toggleSection(key); + const renderSectionSentinel = (key: string) => stickyHeaders ? ( +
{ sectionHeaderSentinelRefs.current.set(key, element); }} + data-model-section-key={key} + className="pointer-events-none absolute top-0 h-px w-full" + aria-hidden="true" + /> + ) : null; + const renderSectionHeader = (key: string, icon: React.ReactNode, label: React.ReactNode, headerDragProps?: SortableFavoriteHandleProps) => { const collapsed = isSectionCollapsed(key); const toggleKeyDown = (event: React.KeyboardEvent) => { @@ -703,6 +782,7 @@ export const ModelPickerList: React.FC = ({ tabIndex={0} aria-expanded={!collapsed} title={reorderProviderTitle} + data-model-picker-sticky-header={stickyHeaders ? 'true' : undefined} className={cn(headerClassName, 'w-full text-left cursor-grab select-none active:cursor-grabbing')} onClick={() => toggleSectionCollapsed(key)} onKeyDown={toggleKeyDown} @@ -720,6 +800,7 @@ export const ModelPickerList: React.FC = ({ return ( -
- )} - - )} - {dockBadgeSupported && ( (renamingFolderId || editingIdRef.current) ? false : true} style={{ - backgroundColor: 'var(--surface-elevated)', color: 'var(--surface-elevated-foreground)', }} className={cn(dropdownMenuPopupClass, 'min-w-[180px]')} diff --git a/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx b/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx index c0622515..e67decf0 100644 --- a/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx @@ -218,7 +218,7 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode { hideTopScrollShadow={!enableStickyFade} scrollShadowSize={96} outerClassName="flex-1 min-h-0" - className={cn('oc-sidebar-scroller space-y-1.5 pb-1 pl-2.5 pr-2 [overflow-anchor:none]', props.mobileVariant ? '' : '')} + className={cn('oc-sidebar-scroller oc-sticky-fade-scroller space-y-1.5 pb-1 pl-2.5 pr-2 [overflow-anchor:none]', props.mobileVariant ? '' : '')} style={enableStickyFade ? { '--scroll-shadow-top-size': '0px' } as React.CSSProperties : undefined} onScroll={enableStickyFade ? (event) => syncTopFade(event.currentTarget) : undefined} > diff --git a/packages/ui/src/components/ui/context-menu.tsx b/packages/ui/src/components/ui/context-menu.tsx index 9812f3fc..265eb4e5 100644 --- a/packages/ui/src/components/ui/context-menu.tsx +++ b/packages/ui/src/components/ui/context-menu.tsx @@ -29,7 +29,6 @@ function ContextMenuContent({ className, positionerClassName, children, style, . } data-slot="dialog-overlay" className={cn( - "fixed inset-0 z-50 bg-black/50 dark:bg-black/75", + "oc-glass-backdrop fixed inset-0 z-50 bg-black/25 dark:bg-black/40", "transition-opacity duration-150 ease-out", "data-[starting-style]:opacity-0 data-[ending-style]:opacity-0", className @@ -93,7 +93,7 @@ function DialogContent({ data-slot="dialog-content" data-state-slot="dialog" className={cn( - "relative pointer-events-auto bg-background text-foreground flex flex-col w-full max-w-lg max-h-full gap-4 rounded-xl border p-6 shadow-none overflow-y-auto pwa-dialog-content origin-center", + "oc-glass-dialog relative pointer-events-auto text-foreground flex flex-col w-full max-w-lg max-h-full gap-4 rounded-xl border p-6 shadow-none overflow-y-auto pwa-dialog-content origin-center", "transition-all duration-150 ease-out", "data-[starting-style]:opacity-0 data-[starting-style]:scale-[0.98]", "data-[ending-style]:opacity-0 data-[ending-style]:scale-[0.98]", diff --git a/packages/ui/src/components/ui/dropdown-menu.styles.ts b/packages/ui/src/components/ui/dropdown-menu.styles.ts index 520dee1d..1dce1ef4 100644 --- a/packages/ui/src/components/ui/dropdown-menu.styles.ts +++ b/packages/ui/src/components/ui/dropdown-menu.styles.ts @@ -1,4 +1,4 @@ -export const dropdownMenuPopupClass = "app-region-no-drag transition-all duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 z-50 max-h-[var(--available-height)] min-w-[8rem] origin-[var(--transform-origin)] overflow-visible rounded-xl p-1 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]"; +export const dropdownMenuPopupClass = "oc-glass-popover oc-glass-floating app-region-no-drag transition-all duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 z-50 max-h-[var(--available-height)] min-w-[8rem] origin-[var(--transform-origin)] overflow-visible rounded-xl p-1"; export const dropdownMenuItemClass = "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-destructive/10 dark:data-[variant=destructive]:hover:bg-destructive/20 data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5"; export const dropdownMenuSubTriggerClass = "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5"; export const dropdownMenuSeparatorClass = "bg-border -mx-1 my-0.5 h-px"; diff --git a/packages/ui/src/components/ui/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx index 16215d81..fa70b1a7 100644 --- a/packages/ui/src/components/ui/dropdown-menu.tsx +++ b/packages/ui/src/components/ui/dropdown-menu.tsx @@ -123,7 +123,6 @@ function DropdownMenuContent({ = ({ open, onOpenChan = ({ open, onOpenChan 'relative pointer-events-auto', 'w-[90vw] max-w-[1200px] h-[85vh] max-h-[900px]', 'rounded-xl border shadow-none overflow-hidden origin-center', - 'bg-background', + 'oc-glass-dialog', 'transition-all duration-150 ease-out', 'data-[starting-style]:opacity-0 data-[starting-style]:scale-[0.98]', 'data-[ending-style]:opacity-0 data-[ending-style]:scale-[0.98]', diff --git a/packages/ui/src/contexts/ThemeSystemContext.tsx b/packages/ui/src/contexts/ThemeSystemContext.tsx index 516676b4..7e5ef975 100644 --- a/packages/ui/src/contexts/ThemeSystemContext.tsx +++ b/packages/ui/src/contexts/ThemeSystemContext.tsx @@ -347,9 +347,7 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro if (typeof document === 'undefined') { return; } - const hasMacVibrancy = document.documentElement.hasAttribute('data-oc-vibrancy') - || window.__OPENCHAMBER_ELECTRON__?.macVibrancy === true; - const chromeColor = hasMacVibrancy ? 'transparent' : theme.colors.surface.background; + const chromeColor = theme.colors.surface.background; document.body.style.backgroundColor = chromeColor; diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index b6462769..7802822c 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -52,63 +52,6 @@ button, transition: background-color 180ms ease; } -/* ── 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; - } -} - /* Ensure interactive controls consistently show pointer cursor */ :where( button, @@ -348,7 +291,7 @@ div[data-chat-input-footer="true"] { ); } -/* Sticky-fade overlay (desktop sidebar): the crisp duplicate identity stays +/* Sticky-fade overlay: the crisp duplicate identity stays mounted and its visibility is driven synchronously by the scroller's own top-scroll data attribute — the same signal that engages the mask — so the overlay appears in the exact frame the mask hides the real header (no blink). @@ -358,8 +301,8 @@ div[data-chat-input-footer="true"] { opacity: 0; } -.oc-sticky-fade-root:has(.oc-sidebar-scroller[data-top-scroll="true"]) .oc-sticky-fade-overlay, -.oc-sticky-fade-root:has(.oc-sidebar-scroller[data-top-bottom-scroll="true"]) .oc-sticky-fade-overlay { +.oc-sticky-fade-root:has(.oc-sticky-fade-scroller[data-top-scroll="true"]) .oc-sticky-fade-overlay, +.oc-sticky-fade-root:has(.oc-sticky-fade-scroller[data-top-bottom-scroll="true"]) .oc-sticky-fade-overlay { opacity: 1; } diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index 7f4bb0a2..ed3bf94f 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -241,8 +241,6 @@ type DesktopBridgeGlobal = { type ElectronRuntimeGlobal = { runtime?: string; arch?: string; - macVibrancy?: boolean; - macVibrancySupported?: boolean; trayEnabled?: boolean; }; diff --git a/packages/ui/src/lib/i18n/messages/de.settings.ts b/packages/ui/src/lib/i18n/messages/de.settings.ts index ef0d6e87..f405d959 100644 --- a/packages/ui/src/lib/i18n/messages/de.settings.ts +++ b/packages/ui/src/lib/i18n/messages/de.settings.ts @@ -1810,8 +1810,6 @@ export const settingsDict = { 'settings.openchamber.visual.field.selectWeekStartAria': 'Wochenstart auswählen', 'settings.openchamber.visual.actions.reloadThemes': 'Designs neu laden', 'settings.openchamber.visual.actions.reloadingThemes': 'Designs werden neu geladen...', - 'settings.openchamber.visual.field.macVibrancy': 'Fenstertransparenz', - 'settings.openchamber.visual.field.macVibrancyHint': 'Verwenden Sie den nativen macOS-Blur (Vibrancy) hinter der Seitenleiste. Ausschalten für vollständig solide, undurchsichtige Oberflächen.', 'settings.openchamber.visual.field.dockBadge': 'Dock-Badge', 'settings.openchamber.visual.field.dockBadgeHint': 'Zeigt eine Anzahl an Chats mit nicht gesehenem Aktivität auf dem macOS-Dock-Icon an.', 'settings.openchamber.visual.actions.saveAndRestart': 'Speichern & Neustart', diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 5d9b629c..e43c5bee 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1886,8 +1886,6 @@ 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.field.dockBadge': 'Dock badge', 'settings.openchamber.visual.field.dockBadgeHint': 'Show a count of chats with unseen activity on the macOS dock icon.', 'settings.openchamber.visual.actions.saveAndRestart': 'Save & restart', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index bc0e9cb6..3fcf7cf0 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1862,8 +1862,6 @@ 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.field.dockBadge": "Insignia en el Dock", "settings.openchamber.visual.field.dockBadgeHint": "Muestra en el icono del Dock de macOS el número de chats con actividad sin ver.", "settings.openchamber.visual.actions.saveAndRestart": "Guardar y reiniciar", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index fd6049ca..d5f08239 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -2083,8 +2083,6 @@ export const settingsDict = { 'settings.openchamber.visual.section.mobileLayout': 'Mise en page mobile', 'settings.openchamber.visual.option.mobileLayout.default': 'Ancienne', 'settings.openchamber.visual.option.mobileLayout.new': 'Nouvelle', - 'settings.openchamber.visual.field.macVibrancy': 'Transparence de la fenêtre', - 'settings.openchamber.visual.field.macVibrancyHint': 'Utiliser le flou macOS natif (vibrancy) derrière la barre latérale. Désactivez cette option pour des surfaces entièrement opaques.', 'settings.openchamber.visual.field.dockBadge': 'Badge du Dock', 'settings.openchamber.visual.field.dockBadgeHint': 'Afficher sur l’icône du Dock de macOS le nombre de discussions avec une activité non vue.', 'settings.openchamber.visual.actions.saveAndRestart': 'Enregistrer et redémarrer', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index 13273aab..ce1c06d5 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -1896,8 +1896,6 @@ export const settingsDict = { 'settings.openchamber.visual.field.selectWeekStartAria': '週の開始日を選択', 'settings.openchamber.visual.actions.reloadThemes': 'テーマを再読み込み', 'settings.openchamber.visual.actions.reloadingThemes': 'テーマを再読み込み中...', - 'settings.openchamber.visual.field.macVibrancy': 'ウィンドウ透過', - 'settings.openchamber.visual.field.macVibrancyHint': 'サイドバーの背後に macOS ネイティブのぼかし(Vibrancy)を使用します。完全に不透明なサーフェスにする場合はオフにします。', 'settings.openchamber.visual.field.dockBadge': 'Dock バッジ', 'settings.openchamber.visual.field.dockBadgeHint': '未読のアクティビティがあるチャットの数を macOS の Dock アイコンに表示します。', 'settings.openchamber.visual.actions.saveAndRestart': '保存して再起動', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index f637fc5d..1ca7fa88 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1862,8 +1862,6 @@ 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.field.dockBadge': 'Dock 배지', 'settings.openchamber.visual.field.dockBadgeHint': '확인하지 않은 활동이 있는 채팅 수를 macOS Dock 아이콘에 표시합니다.', 'settings.openchamber.visual.actions.saveAndRestart': '저장 후 재시작', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 5b94cd50..238fbec3 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -1004,8 +1004,6 @@ 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.field.dockBadge': 'Plakietka w Docku', 'settings.openchamber.visual.field.dockBadgeHint': 'Pokazuj na ikonie w Docku macOS liczbę czatów z nieprzeczytaną aktywnością.', 'settings.openchamber.visual.actions.saveAndRestart': 'Zapisz i uruchom ponownie', 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 1380335b..f78f4611 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1862,8 +1862,6 @@ 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.field.dockBadge": "Selo no Dock", "settings.openchamber.visual.field.dockBadgeHint": "Mostra no ícone do Dock do macOS o número de conversas com atividade não vista.", "settings.openchamber.visual.actions.saveAndRestart": "Salvar e reiniciar", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 2fd58376..01d82066 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1862,8 +1862,6 @@ 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.field.dockBadge": "Лічильник у доку", "settings.openchamber.visual.field.dockBadgeHint": "Показувати на іконці в доку кількість чатів із непрочитаною активністю.", "settings.openchamber.visual.actions.saveAndRestart": "Зберегти та перезапустити", 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 2703eab8..b8ecdb1a 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1862,8 +1862,6 @@ 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.field.dockBadge': 'Dock 角标', 'settings.openchamber.visual.field.dockBadgeHint': '在 macOS Dock 图标上显示有未读动态的会话数量。', 'settings.openchamber.visual.actions.saveAndRestart': '保存并重启', 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 58dfb2b4..116827ad 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1766,8 +1766,6 @@ '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.field.dockBadge': 'Dock 標記', 'settings.openchamber.visual.field.dockBadgeHint': '在 macOS Dock 圖示上顯示有未讀動態的對話數量。', 'settings.openchamber.visual.actions.saveAndRestart': '儲存並重新啟動', diff --git a/packages/ui/src/lib/settings/search.ts b/packages/ui/src/lib/settings/search.ts index eecca22c..55f364cc 100644 --- a/packages/ui/src/lib/settings/search.ts +++ b/packages/ui/src/lib/settings/search.ts @@ -65,14 +65,6 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [ keywords: ['theme', 'color', 'dark mode'], isAvailable: (ctx) => !ctx.isVSCode, }, - { - id: 'appearance.window-transparency', - page: 'appearance', - titleKey: 'settings.openchamber.visual.field.macVibrancy', - descriptionKey: 'settings.openchamber.visual.field.macVibrancyHint', - keywords: ['transparent', 'transparency', 'vibrancy', 'blur', 'macos', 'opaque'], - isAvailable: (ctx) => ctx.isDesktopLocalOrigin, - }, { id: 'appearance.dock-badge', page: 'appearance', diff --git a/packages/ui/src/lib/theme/cssGenerator.ts b/packages/ui/src/lib/theme/cssGenerator.ts index db202bc7..a8cab969 100644 --- a/packages/ui/src/lib/theme/cssGenerator.ts +++ b/packages/ui/src/lib/theme/cssGenerator.ts @@ -123,9 +123,6 @@ 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( @@ -134,9 +131,6 @@ 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( @@ -145,9 +139,6 @@ 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)) { @@ -196,18 +187,6 @@ const sidebarBaseRgb = hexToRgb(theme.colors.surface.muted); 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 9acb85e6..60729bb8 100644 --- a/packages/ui/src/styles/design-system.css +++ b/packages/ui/src/styles/design-system.css @@ -16,6 +16,12 @@ --oc-scrollbar-thumb: oklch(0.32 0.03 50 / 0.4); --oc-scrollbar-thumb-hover: oklch(0.32 0.03 50 / 0.6); --padding-scale: 1; + /* Shared glass surfaces. Semantic surface tokens provide each tint. */ + --oc-glass-blur: 22px; + --oc-glass-opacity: 52%; + --oc-glass-popover-opacity: 50%; + --oc-glass-tooltip-opacity: 62%; + --oc-glass-saturation: 1.24; /* Semantic typography defaults (must match SEMANTIC_TYPOGRAPHY) */ --text-markdown: 0.9375rem; @@ -63,6 +69,11 @@ } .dark { + --oc-glass-blur: 26px; + --oc-glass-opacity: 50%; + --oc-glass-popover-opacity: 52%; + --oc-glass-tooltip-opacity: 64%; + --oc-glass-saturation: 1.16; /* Default Dark Theme - using OKLCH color space */ --background: oklch(0.16 0.01 30); /* #151313 */ --foreground: oklch(0.85 0.02 90); /* #cdccc3 */ @@ -133,13 +144,6 @@ 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; } @@ -199,6 +203,90 @@ } } +@layer components { + /* Shared translucent surfaces. Each variant only chooses its semantic tint; + blur, accessibility fallbacks, and floating-surface elevation stay common. + Utilities remain able to opt an individual consumer out. */ + .oc-glass-popover, +.oc-glass-tooltip, +.oc-glass-dialog, +.oc-glass-panel { + background-color: color-mix(in srgb, var(--oc-glass-color) var(--oc-glass-opacity), transparent); + -webkit-backdrop-filter: blur(var(--oc-glass-blur)) saturate(var(--oc-glass-saturation)); + backdrop-filter: blur(var(--oc-glass-blur)) saturate(var(--oc-glass-saturation)); +} + +.oc-glass-popover { + --oc-glass-color: var(--surface-elevated); + --oc-glass-opacity: var(--oc-glass-popover-opacity); +} + +.oc-glass-tooltip { + --oc-glass-color: var(--surface-elevated); + --oc-glass-opacity: var(--oc-glass-tooltip-opacity); +} + +.oc-glass-dialog { + --oc-glass-color: var(--surface-background); +} + +.oc-glass-panel { + --oc-glass-color: var(--surface-muted); +} + +.oc-glass-floating { + box-shadow: + inset 0 1px 0 0 rgb(255 255 255 / 0.8), + inset 0 0 0 1px rgb(0 0 0 / 0.04), + 0 0 0 1px rgb(0 0 0 / 0.1), + 0 1px 2px -0.5px rgb(0 0 0 / 0.08), + 0 4px 8px -2px rgb(0 0 0 / 0.08), + 0 12px 20px -4px rgb(0 0 0 / 0.08); +} + +.dark .oc-glass-floating { + box-shadow: + inset 0 1px 0 0 rgb(255 255 255 / 0.12), + inset 0 0 0 1px rgb(255 255 255 / 0.08), + 0 0 0 1px rgb(0 0 0 / 0.36), + 0 1px 1px -0.5px rgb(0 0 0 / 0.22), + 0 3px 3px -1.5px rgb(0 0 0 / 0.2), + 0 6px 6px -3px rgb(0 0 0 / 0.16); +} + +.oc-glass-backdrop { + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); +} + +@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { + .oc-glass-popover, + .oc-glass-tooltip, + .oc-glass-dialog, + .oc-glass-panel { + background-color: var(--oc-glass-color); + } +} + +@media (prefers-reduced-transparency: reduce) { + .oc-glass-popover, + .oc-glass-tooltip, + .oc-glass-dialog, + .oc-glass-panel, + .oc-glass-backdrop { + -webkit-backdrop-filter: none; + backdrop-filter: none; + } + + .oc-glass-popover, + .oc-glass-tooltip, + .oc-glass-dialog, + .oc-glass-panel { + background-color: var(--oc-glass-color); + } +} +} + @theme inline { --radius-none: 0; --radius-sm: 0.25rem; /* 4px */ diff --git a/packages/ui/src/types/desktop.d.ts b/packages/ui/src/types/desktop.d.ts index 37f5fbdf..72a3744a 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; arch?: string; macVibrancy?: boolean; macVibrancySupported?: boolean; trayEnabled?: boolean }; + __OPENCHAMBER_ELECTRON__?: { runtime?: string; arch?: string; trayEnabled?: boolean }; __OPENCHAMBER_PLATFORM__?: string; __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__?: DesktopBootOutcome; }