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; }