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:
Bohdan Triapitsyn
2026-08-10 15:10:37 +03:00
parent 90a16d1d44
commit 2c30af5807
36 changed files with 273 additions and 302 deletions
@@ -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} />