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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -178,13 +178,14 @@ export const WorkStatusPanel: React.FC<Props> = ({ 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)]',
|
||||
|
||||
@@ -336,7 +336,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-[min(27rem,calc(100vw-2rem))] max-h-[75vh] overflow-y-auto bg-[var(--surface-elevated)] p-0"
|
||||
className="w-[min(27rem,calc(100vw-2rem))] max-h-[75vh] overflow-y-auto p-0"
|
||||
>
|
||||
{isDesktopApp ? (
|
||||
<div>
|
||||
|
||||
@@ -127,7 +127,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children, cl
|
||||
ref={sidebarRef}
|
||||
className={cn(
|
||||
'relative flex h-full overflow-hidden border-r border-border will-change-[width] motion-reduce:transition-none',
|
||||
'bg-sidebar oc-vibrancy-surface',
|
||||
'bg-sidebar',
|
||||
!isOpen && 'border-r-0',
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -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<HTMLDivElement | null>(null);
|
||||
@@ -129,10 +128,6 @@ export const TitlebarLeftControls: React.FC = () => {
|
||||
<ProjectActionsButton
|
||||
projectRef={projectActionsContext.projectRef}
|
||||
directory={projectActionsContext.directory}
|
||||
// While the sidebar is open the controls sit over the frosted
|
||||
// sidebar — let the pill share its translucency instead of painting
|
||||
// an opaque surface (handled under [data-oc-vibrancy] in CSS).
|
||||
className={isSidebarOpen ? 'oc-vibrancy-pill' : undefined}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -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<ModelPickerListProps> = ({
|
||||
const selectionStore = selectionStoreRef.current;
|
||||
const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
|
||||
const scrollRef = React.useRef<HTMLElement | null>(null);
|
||||
const sectionHeaderSentinelRefs = React.useRef<Map<string, HTMLDivElement | null>>(new Map());
|
||||
const stickyFadeSizeRef = React.useRef(0);
|
||||
const [stuckSectionHeaders, setStuckSectionHeaders] = React.useState<Set<string>>(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<ModelPickerListProps> = ({
|
||||
})
|
||||
.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<HTMLDivElement> | React.PointerEvent<HTMLDivElement>,
|
||||
) => {
|
||||
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<ModelPickerListProps> = ({
|
||||
|
||||
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<ModelPickerListProps> = ({
|
||||
const isSectionCollapsed = (key: string) => collapsedSections.has(key);
|
||||
const toggleSectionCollapsed = (key: string) => toggleSection(key);
|
||||
|
||||
const renderSectionSentinel = (key: string) => stickyHeaders ? (
|
||||
<div
|
||||
ref={(element) => { 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<ModelPickerListProps> = ({
|
||||
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<ModelPickerListProps> = ({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-model-picker-sticky-header={stickyHeaders ? 'true' : undefined}
|
||||
className={cn(headerClassName, 'w-full text-left cursor-pointer')}
|
||||
onClick={() => toggleSectionCollapsed(key)}
|
||||
aria-expanded={!collapsed}
|
||||
@@ -737,15 +818,43 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
|
||||
provider: (typeof filteredProviders)[number],
|
||||
providerIndex: number,
|
||||
headerDragProps?: SortableFavoriteHandleProps,
|
||||
) => (
|
||||
<>
|
||||
{providerIndex > 0 ? <div className="h-px bg-border/40 my-1" /> : null}
|
||||
{renderSectionHeader(`provider:${provider.id}`, <ProviderLogo providerId={provider.id} className="h-4 w-4 flex-shrink-0" />, provider.name || provider.id, headerDragProps)}
|
||||
{!isSectionCollapsed(`provider:${provider.id}`)
|
||||
? provider.models.map((model) => renderRow({ model, providerID: provider.id, modelID: model.id as string }, 'provider', false, currentFlatIndex++))
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
) => {
|
||||
const sectionKey = `provider:${provider.id}`;
|
||||
return (
|
||||
<>
|
||||
{providerIndex > 0 ? <div className="h-px bg-border/40 my-1" /> : null}
|
||||
<div className="relative">
|
||||
{renderSectionSentinel(sectionKey)}
|
||||
{renderSectionHeader(sectionKey, <ProviderLogo providerId={provider.id} className="h-4 w-4 flex-shrink-0" />, provider.name || provider.id, headerDragProps)}
|
||||
{!isSectionCollapsed(sectionKey)
|
||||
? provider.models.map((model) => renderRow({ model, providerID: provider.id, modelID: model.id as string }, 'provider', false, currentFlatIndex++))
|
||||
: null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
let stuckSectionKey: string | null = null;
|
||||
for (const sectionKey of visibleSectionKeys) {
|
||||
if (stuckSectionHeaders.has(sectionKey)) stuckSectionKey = sectionKey;
|
||||
}
|
||||
// The sidebar can seed its overlay with the first section because its first
|
||||
// header starts flush with the scroller. `Not selected` may precede the
|
||||
// first model section here, so wait for that section's sentinel rather than
|
||||
// showing its identity while the leading action is still visible.
|
||||
const leadingSectionKey = stuckSectionKey ?? (!includeNotSelected ? visibleSectionKeys[0] ?? null : null);
|
||||
const renderSectionIdentity = (sectionKey: string): React.ReactNode => {
|
||||
if (sectionKey === 'favorites') {
|
||||
return <><Icon name="star-fill" className="h-4 w-4 flex-shrink-0 text-primary" /><span className="min-w-0 truncate">{labels.favorites}</span></>;
|
||||
}
|
||||
if (sectionKey === 'recent') {
|
||||
return <><Icon name="time" className="h-4 w-4 flex-shrink-0" /><span className="min-w-0 truncate">{labels.recent}</span></>;
|
||||
}
|
||||
const providerId = sectionKey.startsWith('provider:') ? sectionKey.slice('provider:'.length) : '';
|
||||
const provider = providerById.get(providerId);
|
||||
if (!provider) return null;
|
||||
return <><ProviderLogo providerId={providerId} className="h-4 w-4 flex-shrink-0" /><span className="min-w-0 truncate">{provider.name || provider.id}</span></>;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -764,7 +873,25 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollableOverlay ref={scrollRef} outerClassName={maxHeightClassName} className="overlay-scrollbar-target--no-gutter" style={maxHeightStyle}>
|
||||
<div
|
||||
className="oc-sticky-fade-root relative flex min-h-0 flex-1"
|
||||
onPointerDownCapture={stickyHeaders ? blockStickyFadeInteraction : undefined}
|
||||
onClickCapture={stickyHeaders ? blockStickyFadeInteraction : undefined}
|
||||
onContextMenuCapture={stickyHeaders ? blockStickyFadeInteraction : undefined}
|
||||
>
|
||||
<ScrollableOverlay
|
||||
ref={scrollRef}
|
||||
useScrollShadow={stickyHeaders}
|
||||
hideTopScrollShadow={!stickyHeaders}
|
||||
scrollShadowSize={96}
|
||||
outerClassName={maxHeightClassName}
|
||||
className="oc-sticky-fade-scroller overlay-scrollbar-target--no-gutter"
|
||||
style={{
|
||||
...(stickyHeaders ? { '--scroll-shadow-top-size': '0px' } as React.CSSProperties : {}),
|
||||
...maxHeightStyle,
|
||||
}}
|
||||
onScroll={stickyHeaders ? (event) => syncStickyFade(event.currentTarget) : undefined}
|
||||
>
|
||||
<div className="px-1">
|
||||
{includeNotSelected ? (
|
||||
<>
|
||||
@@ -786,7 +913,8 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
|
||||
) : null}
|
||||
|
||||
{filteredFavorites.length > 0 ? (
|
||||
<div>
|
||||
<div className="relative">
|
||||
{renderSectionSentinel('favorites')}
|
||||
{renderSectionHeader('favorites', <Icon name="star-fill" className="h-4 w-4 text-primary" />, labels.favorites)}
|
||||
{!isSectionCollapsed('favorites') && (favoriteSortingEnabled ? (
|
||||
<DndContext sensors={favoriteRowSensors} collisionDetection={closestCenter} onDragEnd={handleFavoriteDragEnd}>
|
||||
@@ -806,11 +934,14 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
|
||||
) : null}
|
||||
|
||||
{filteredRecents.length > 0 ? (
|
||||
<div>
|
||||
<>
|
||||
{filteredFavorites.length > 0 ? <div className="h-px bg-border/40 my-1" /> : null}
|
||||
{renderSectionHeader('recent', <Icon name="time" className="h-4 w-4" />, labels.recent)}
|
||||
{!isSectionCollapsed('recent') ? filteredRecents.map((entry) => renderRow(entry, 'recent', true, currentFlatIndex++)) : null}
|
||||
</div>
|
||||
<div className="relative">
|
||||
{renderSectionSentinel('recent')}
|
||||
{renderSectionHeader('recent', <Icon name="time" className="h-4 w-4" />, labels.recent)}
|
||||
{!isSectionCollapsed('recent') ? filteredRecents.map((entry) => renderRow(entry, 'recent', true, currentFlatIndex++)) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{(filteredFavorites.length > 0 || filteredRecents.length > 0) && filteredProviders.length > 0 ? <div className="h-px bg-border/40 my-1" /> : null}
|
||||
@@ -834,6 +965,15 @@ export const ModelPickerList: React.FC<ModelPickerListProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</ScrollableOverlay>
|
||||
{stickyHeaders && leadingSectionKey ? (
|
||||
<div
|
||||
className="oc-sticky-fade-overlay pointer-events-none absolute inset-x-0 top-0 z-30 flex items-center gap-2 px-3 py-1.5 typography-micro font-semibold uppercase tracking-wider text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{renderSectionIdentity(leadingSectionKey)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="px-3 pt-1 pb-1.5 border-t border-border/40 typography-micro text-muted-foreground">
|
||||
<ModelPickerFooter store={selectionStore} flatModelList={flatModelList} footerContent={footerContent} fallback={labels.keyboardHint} />
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import {
|
||||
invokeDesktop,
|
||||
isDesktopShell,
|
||||
isVSCodeRuntime,
|
||||
isWebRuntime,
|
||||
@@ -410,16 +409,6 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
const [themesReloading, setThemesReloading] = React.useState(false);
|
||||
|
||||
// macOS-desktop-only vibrancy toggle. Changing it needs a full relaunch
|
||||
// (vibrancy is a window-creation option), so we persist + restart on save.
|
||||
const macVibrancySupported = React.useMemo(
|
||||
() => isDesktopShell() && typeof window !== 'undefined' && window.__OPENCHAMBER_ELECTRON__?.macVibrancySupported === true,
|
||||
[],
|
||||
);
|
||||
const macVibrancyEnabled = typeof window !== 'undefined' && window.__OPENCHAMBER_ELECTRON__?.macVibrancy === true;
|
||||
const [vibrancyChecked, setVibrancyChecked] = React.useState(macVibrancyEnabled);
|
||||
const [vibrancyRestarting, setVibrancyRestarting] = React.useState(false);
|
||||
|
||||
// macOS-desktop-only dock badge that counts chats with unseen activity.
|
||||
// The tray sync (mac-only) pumps the count to the main process, so the
|
||||
// toggle is offered only where it actually has an effect. No relaunch needed.
|
||||
@@ -979,36 +968,6 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</div>
|
||||
</SettingsTwoColumn>
|
||||
|
||||
{macVibrancySupported && (
|
||||
<SettingsInset settingsItem="appearance.window-transparency" className="flex flex-col gap-1.5">
|
||||
<SettingsCheckboxRow
|
||||
checked={vibrancyChecked}
|
||||
onChange={setVibrancyChecked}
|
||||
disabled={vibrancyRestarting}
|
||||
label={t('settings.openchamber.visual.field.macVibrancy')}
|
||||
info={t('settings.openchamber.visual.field.macVibrancyHint')}
|
||||
ariaLabel={t('settings.openchamber.visual.field.macVibrancy')}
|
||||
/>
|
||||
{vibrancyChecked !== macVibrancyEnabled && (
|
||||
<div className="pl-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={vibrancyRestarting}
|
||||
onClick={() => {
|
||||
setVibrancyRestarting(true);
|
||||
void invokeDesktop('desktop_set_vibrancy', { enabled: vibrancyChecked });
|
||||
}}
|
||||
>
|
||||
{vibrancyRestarting
|
||||
? t('settings.openchamber.visual.actions.restarting')
|
||||
: t('settings.openchamber.visual.actions.saveAndRestart')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</SettingsInset>
|
||||
)}
|
||||
|
||||
{dockBadgeSupported && (
|
||||
<SettingsInset settingsItem="appearance.dock-badge">
|
||||
<SettingsCheckboxRow
|
||||
|
||||
@@ -1143,7 +1143,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
data-slot="dropdown-menu-content"
|
||||
finalFocus={() => (renamingFolderId || editingIdRef.current) ? false : true}
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-elevated)',
|
||||
color: 'var(--surface-elevated-foreground)',
|
||||
}}
|
||||
className={cn(dropdownMenuPopupClass, 'min-w-[180px]')}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,6 @@ function ContextMenuContent({ className, positionerClassName, children, style, .
|
||||
<BaseContextMenu.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
style={{
|
||||
backgroundColor: "var(--surface-elevated)",
|
||||
color: "var(--surface-elevated-foreground)",
|
||||
...style,
|
||||
}}
|
||||
|
||||
@@ -61,7 +61,7 @@ const DialogOverlay = React.forwardRef<
|
||||
ref={ref as React.Ref<HTMLDivElement>}
|
||||
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]",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -123,7 +123,6 @@ function DropdownMenuContent({
|
||||
<BaseMenu.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-elevated)',
|
||||
color: 'var(--surface-elevated-foreground)',
|
||||
...style,
|
||||
}}
|
||||
@@ -280,7 +279,6 @@ function DropdownMenuSubContent({
|
||||
<BaseMenu.Popup
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-elevated)',
|
||||
color: 'var(--surface-elevated-foreground)',
|
||||
}}
|
||||
className={cn(
|
||||
|
||||
@@ -192,11 +192,10 @@ function SelectContent({
|
||||
<BaseSelect.Popup
|
||||
data-slot="select-content"
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-elevated)',
|
||||
color: 'var(--surface-elevated-foreground)',
|
||||
}}
|
||||
className={cn(
|
||||
"pointer-events-auto 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 relative z-[120] max-h-[var(--available-height)] min-w-[8rem] origin-[var(--transform-origin)] overflow-x-hidden rounded-xl 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)]",
|
||||
"oc-glass-popover oc-glass-floating pointer-events-auto 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 relative z-[120] max-h-[var(--available-height)] min-w-[8rem] origin-[var(--transform-origin)] overflow-x-hidden rounded-xl",
|
||||
!alignItemWithTrigger &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
fitContent && "w-max min-w-0",
|
||||
|
||||
@@ -277,7 +277,7 @@ function TooltipContent({
|
||||
// data-instant is set when moving between grouped tooltips
|
||||
// (shared TooltipProvider): reposition without replaying the
|
||||
// full exit/enter animation.
|
||||
"bg-[var(--surface-elevated)] text-[var(--surface-elevated-foreground)] border border-border/60 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 data-[instant]:transition-none data-[instant]:duration-0 z-50 w-fit origin-[var(--transform-origin)] rounded-xl px-3 py-1.5 typography-meta text-balance overflow-hidden",
|
||||
"oc-glass-tooltip text-[var(--surface-elevated-foreground)] border border-border/60 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 data-[instant]:transition-none data-[instant]:duration-0 z-50 w-fit origin-[var(--transform-origin)] rounded-xl px-3 py-1.5 typography-meta text-balance overflow-hidden",
|
||||
className
|
||||
)}
|
||||
style={{ ...style }}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChan
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop
|
||||
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',
|
||||
)}
|
||||
@@ -50,7 +50,7 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ 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]',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -241,8 +241,6 @@ type DesktopBridgeGlobal = {
|
||||
type ElectronRuntimeGlobal = {
|
||||
runtime?: string;
|
||||
arch?: string;
|
||||
macVibrancy?: boolean;
|
||||
macVibrancySupported?: boolean;
|
||||
trayEnabled?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '保存して再起動',
|
||||
|
||||
@@ -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': '저장 후 재시작',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Зберегти та перезапустити",
|
||||
|
||||
@@ -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': '保存并重启',
|
||||
|
||||
@@ -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': '儲存並重新啟動',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Vendored
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user