fix(ui): smooth desktop sidebar sticky transitions (#2528)
* fix(ui): blend vibrant sidebar transitions * fix(ui): fade sidebar content below header * fix(ui): smooth vibrant sidebar sticky headers * fix(ui): scope sticky fade to desktop * fix(ui): respect sticky header preference
This commit is contained in:
@@ -1121,6 +1121,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
|||||||
const showRecentSection = useSessionDisplayStore((state) => state.showRecentSection);
|
const showRecentSection = useSessionDisplayStore((state) => state.showRecentSection);
|
||||||
const showArchivedSessions = useSessionDisplayStore((state) => state.showArchivedSessions);
|
const showArchivedSessions = useSessionDisplayStore((state) => state.showArchivedSessions);
|
||||||
const projectSortOrder = useSessionDisplayStore((state) => state.projectSortOrder);
|
const projectSortOrder = useSessionDisplayStore((state) => state.projectSortOrder);
|
||||||
|
const stickyZoneHeaders = useSessionDisplayStore((state) => state.stickyZoneHeaders);
|
||||||
const manualProjectOrder = useProjectsStore((state) => state.manualProjectOrder);
|
const manualProjectOrder = useProjectsStore((state) => state.manualProjectOrder);
|
||||||
const projectExpandedParentsRef = React.useRef<Set<string>>(new Set());
|
const projectExpandedParentsRef = React.useRef<Set<string>>(new Set());
|
||||||
const recentExpandedParentsRef = React.useRef<Set<string>>(new Set());
|
const recentExpandedParentsRef = React.useRef<Set<string>>(new Set());
|
||||||
@@ -1516,7 +1517,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
|||||||
const headerActionButtonClass = mobileVariant ? mobileHeaderActionButtonClass : desktopHeaderActionButtonClass;
|
const headerActionButtonClass = mobileVariant ? mobileHeaderActionButtonClass : desktopHeaderActionButtonClass;
|
||||||
const headerActionIconClass = 'h-4.5 w-4.5';
|
const headerActionIconClass = 'h-4.5 w-4.5';
|
||||||
const stuckProjectHeaders = useStickyProjectHeaders({
|
const stuckProjectHeaders = useStickyProjectHeaders({
|
||||||
enabled: isVisible,
|
enabled: isVisible && stickyZoneHeaders,
|
||||||
isDesktopShellRuntime,
|
isDesktopShellRuntime,
|
||||||
projectSections,
|
projectSections,
|
||||||
projectHeaderSentinelRefs,
|
projectHeaderSentinelRefs,
|
||||||
@@ -1861,6 +1862,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
|
|||||||
hideDirectoryControls={hideDirectoryControls}
|
hideDirectoryControls={hideDirectoryControls}
|
||||||
projectRepoStatus={projectRepoStatus}
|
projectRepoStatus={projectRepoStatus}
|
||||||
isDesktopShellRuntime={isDesktopShellRuntime}
|
isDesktopShellRuntime={isDesktopShellRuntime}
|
||||||
|
stickyZoneHeaders={stickyZoneHeaders}
|
||||||
stuckProjectHeaders={stuckProjectHeaders}
|
stuckProjectHeaders={stuckProjectHeaders}
|
||||||
mobileVariant={mobileVariant}
|
mobileVariant={mobileVariant}
|
||||||
alwaysShowActions={alwaysShowSidebarActions}
|
alwaysShowActions={alwaysShowSidebarActions}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
- `SessionSidebar.tsx` now acts mainly as orchestration; core logic moved to focused hooks/components.
|
- `SessionSidebar.tsx` now acts mainly as orchestration; core logic moved to focused hooks/components.
|
||||||
- Layout (web/desktop): top navigation (`SidebarNav`: New session, Scheduled, Multi-run, Archive), then the `recent` zone, then one zone per project with a **flat** session list. There is no rendered worktree grouping level.
|
- Layout (web/desktop): top navigation (`SidebarNav`: New session, Scheduled, Multi-run, Archive), then the `recent` zone, then one zone per project with a **flat** session list. There is no rendered worktree grouping level.
|
||||||
- **Two grouping display modes** (`useSessionDisplayStore.sessionGroupingMode`, toggled in the view dropdown): `'by-worktree'` (default) renders the worktree-grouped `sectionsForRender` with slim PR-aware branch sub-headers inside each project zone; `'flat'` renders `flatSectionsForRender` — one merged non-archived group per project (`id: 'flat'`, `folderScopes` listing every contributing scope) with per-row branch markers. Both derive from the same `projectSections` data layer, which alone feeds bootstrap demand planning and PR polling.
|
- **Two grouping display modes** (`useSessionDisplayStore.sessionGroupingMode`, toggled in the view dropdown): `'by-worktree'` (default) renders the worktree-grouped `sectionsForRender` with slim PR-aware branch sub-headers inside each project zone; `'flat'` renders `flatSectionsForRender` — one merged non-archived group per project (`id: 'flat'`, `folderScopes` listing every contributing scope) with per-row branch markers. Both derive from the same `projectSections` data layer, which alone feeds bootstrap demand planning and PR polling.
|
||||||
- Project headers are sticky background "zone" bands (`SortableProjectItem`); the `recent` section header uses the same band styling. Collapsed projects show an aggregated busy/unseen indicator (`ProjectAggregateStatusIndicator`), derived from the live status index and notification store scoped to the project's directories.
|
- When sticky zone headers are enabled, project headers are sticky "zone" bands (`SortableProjectItem`); on a vibrant desktop the scrolling content fades behind an unmasked, non-interactive copy of the stuck icon/title without painting a background. The transparent fade zone blocks interaction with obscured rows. The `recent` section uses the same overlay while it is the leading sticky header. Collapsed projects show an aggregated busy/unseen indicator (`ProjectAggregateStatusIndicator`), derived from the live status index and notification store scoped to the project's directories.
|
||||||
- Session rows have a single layout (former `minimal`); the `default`/`minimal` display mode was removed (`session-display-mode` store v4 migration drops the key). Rows show an inline branch label (from `node.worktree` or recent's `secondaryMeta`) when the session lives outside the project root, and bold titles while unread.
|
- Session rows have a single layout (former `minimal`); the `default`/`minimal` display mode was removed (`session-display-mode` store v4 migration drops the key). Rows show an inline branch label (from `node.worktree` or recent's `secondaryMeta`) when the session lives outside the project root, and bold titles while unread.
|
||||||
- Folders render **flat** after the loose sessions: nested folders keep `parentId` in the data model but display at one level with a "Parent / Child" path label (`SessionFolderItem.displayName`); collapsing a folder hides its whole subtree. Folder actions resolve their owning scope per folder entry (folders from multiple worktree scopes can coexist under one project).
|
- Folders render **flat** after the loose sessions: nested folders keep `parentId` in the data model but display at one level with a "Parent / Child" path label (`SessionFolderItem.displayName`); collapsing a folder hides its whole subtree. Folder actions resolve their owning scope per folder entry (folders from multiple worktree scopes can coexist under one project).
|
||||||
- Archived sessions are not shown in the web/desktop sidebar; the Archive page (`ArchiveView`, `useUIStore.isArchivePageOpen`) replaces the old toggle. VS Code keeps inline archived buckets behind `showArchivedSessions` (compact webview has no page surfaces). Unarchive is not possible through the upstream OpenCode HTTP API (`session.update` can only set a finite `time.archived`).
|
- Archived sessions are not shown in the web/desktop sidebar; the Archive page (`ArchiveView`, `useUIStore.isArchivePageOpen`) replaces the old toggle. VS Code keeps inline archived buckets behind `showArchivedSessions` (compact webview has no page surfaces). Unarchive is not possible through the upstream OpenCode HTTP API (`session.update` can only set a finite `time.archived`).
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
resolveMenuOpenSessionId,
|
resolveMenuOpenSessionId,
|
||||||
} from './sessionNodeItemUtils';
|
} from './sessionNodeItemUtils';
|
||||||
import type { SessionNodeRenderExtras } from './sessionNodeItemUtils';
|
import type { SessionNodeRenderExtras } from './sessionNodeItemUtils';
|
||||||
import { useStickyHeader } from './hooks/useStickyProjectHeaders';
|
|
||||||
|
|
||||||
type ActivityItem = {
|
type ActivityItem = {
|
||||||
node: SessionNode;
|
node: SessionNode;
|
||||||
@@ -62,17 +61,12 @@ export function SidebarActivitySections(props: Props): React.ReactNode {
|
|||||||
variant = 'section',
|
variant = 'section',
|
||||||
initialVisibleCount = MAX_VISIBLE_RECENT_SESSIONS,
|
initialVisibleCount = MAX_VISIBLE_RECENT_SESSIONS,
|
||||||
batchSize = MAX_VISIBLE_RECENT_SESSIONS,
|
batchSize = MAX_VISIBLE_RECENT_SESSIONS,
|
||||||
isDesktopShellRuntime,
|
|
||||||
} = props;
|
} = props;
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const stickyZoneHeaders = useSessionDisplayStore((state) => state.stickyZoneHeaders);
|
const stickyZoneHeaders = useSessionDisplayStore((state) => state.stickyZoneHeaders);
|
||||||
const [collapsed, setCollapsed] = React.useState<Set<string>>(new Set());
|
const [collapsed, setCollapsed] = React.useState<Set<string>>(new Set());
|
||||||
const [visibleCountBySection, setVisibleCountBySection] = React.useState<Map<string, number>>(new Map());
|
const [visibleCountBySection, setVisibleCountBySection] = React.useState<Map<string, number>>(new Map());
|
||||||
const flatVariant = variant === 'flat';
|
const flatVariant = variant === 'flat';
|
||||||
const { isStuck: isRecentHeaderStuck, sentinelRef: recentHeaderSentinelRef } = useStickyHeader({
|
|
||||||
enabled: stickyZoneHeaders && !flatVariant,
|
|
||||||
isDesktopShellRuntime,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetSectionLimit = React.useCallback((key: string) => {
|
const resetSectionLimit = React.useCallback((key: string) => {
|
||||||
setVisibleCountBySection((prev) => {
|
setVisibleCountBySection((prev) => {
|
||||||
@@ -184,18 +178,10 @@ export function SidebarActivitySections(props: Props): React.ReactNode {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={section.key} className="relative space-y-1">
|
<div key={section.key} className="relative space-y-1">
|
||||||
{/* Zone header styled like a project header band; its solid
|
|
||||||
backing applies only after the header is actually stuck. */}
|
|
||||||
<div
|
|
||||||
ref={recentHeaderSentinelRef}
|
|
||||||
className="absolute top-0 h-px w-full pointer-events-none"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'-ml-2.5 -mr-2',
|
'-ml-2.5 -mr-2',
|
||||||
stickyZoneHeaders && 'sticky top-0 z-20 bg-sidebar',
|
stickyZoneHeaders && 'sticky top-0 z-20 bg-sidebar',
|
||||||
stickyZoneHeaders && isRecentHeaderStuck && 'oc-zone-header-backing',
|
)} data-sidebar-sticky-header={stickyZoneHeaders ? 'true' : undefined}>
|
||||||
)}>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleSection(section.key)}
|
onClick={() => toggleSection(section.key)}
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
|||||||
import { formatDirectoryName, formatPathForDisplay, cn } from '@/lib/utils';
|
import { formatDirectoryName, formatPathForDisplay, cn } from '@/lib/utils';
|
||||||
import type { SessionGroup } from './types';
|
import type { SessionGroup } from './types';
|
||||||
import type { SortableDragHandleProps } from './sortableItems';
|
import type { SortableDragHandleProps } from './sortableItems';
|
||||||
import { SortableGroupItem, SortableProjectItem } from './sortableItems';
|
import { ProjectHeaderIdentity, SortableGroupItem, SortableProjectItem } from './sortableItems';
|
||||||
import { formatProjectLabel } from './utils';
|
import { formatProjectLabel } from './utils';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import type { MainTab } from '@/stores/useUIStore';
|
import type { MainTab } from '@/stores/useUIStore';
|
||||||
import type { ProjectSortOrder } from '@/stores/useSessionDisplayStore';
|
import type { ProjectSortOrder } from '@/stores/useSessionDisplayStore';
|
||||||
import { streamPerfCount } from '@/stores/utils/streamDebug';
|
import { streamPerfCount } from '@/stores/utils/streamDebug';
|
||||||
|
import { Icon } from '@/components/icon/Icon';
|
||||||
|
|
||||||
type ProjectSection = {
|
type ProjectSection = {
|
||||||
project: {
|
project: {
|
||||||
@@ -33,6 +34,18 @@ type ProjectSection = {
|
|||||||
groups: SessionGroup[];
|
groups: SessionGroup[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TOP_FADE_MAX_SIZE = 48;
|
||||||
|
const TOP_FADE_MIN_SIZE = 32;
|
||||||
|
const TOP_FADE_CLEAR_MAX_SIZE = 24;
|
||||||
|
|
||||||
|
const getProjectLabel = (project: ProjectSection['project'], homeDirectory: string | null): string => (
|
||||||
|
formatProjectLabel(
|
||||||
|
project.label?.trim()
|
||||||
|
|| formatDirectoryName(project.normalizedPath, homeDirectory)
|
||||||
|
|| project.normalizedPath,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
topContent?: React.ReactNode;
|
topContent?: React.ReactNode;
|
||||||
sharedSessionsOnly?: boolean;
|
sharedSessionsOnly?: boolean;
|
||||||
@@ -61,6 +74,7 @@ type Props = {
|
|||||||
hideDirectoryControls: boolean;
|
hideDirectoryControls: boolean;
|
||||||
projectRepoStatus: Map<string, boolean | null>;
|
projectRepoStatus: Map<string, boolean | null>;
|
||||||
isDesktopShellRuntime: boolean;
|
isDesktopShellRuntime: boolean;
|
||||||
|
stickyZoneHeaders: boolean;
|
||||||
stuckProjectHeaders: Set<string>;
|
stuckProjectHeaders: Set<string>;
|
||||||
mobileVariant: boolean;
|
mobileVariant: boolean;
|
||||||
alwaysShowActions: boolean;
|
alwaysShowActions: boolean;
|
||||||
@@ -84,6 +98,8 @@ type Props = {
|
|||||||
function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
||||||
streamPerfCount('ui.sidebar_projects_list.render');
|
streamPerfCount('ui.sidebar_projects_list.render');
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const [hasTopScroll, setHasTopScroll] = React.useState(false);
|
||||||
|
const enableStickyFade = props.isDesktopShellRuntime && props.stickyZoneHeaders;
|
||||||
const projectSensors = useSensors(
|
const projectSensors = useSensors(
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
||||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||||
@@ -120,6 +136,46 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
|||||||
// can resolve the scrolling ancestor synchronously (no getComputedStyle
|
// can resolve the scrolling ancestor synchronously (no getComputedStyle
|
||||||
// walk) and skip the cost of a style recalc on every render.
|
// walk) and skip the cost of a style recalc on every render.
|
||||||
const scrollContainerRef = React.useRef<HTMLElement | null>(null);
|
const scrollContainerRef = React.useRef<HTMLElement | null>(null);
|
||||||
|
// Keep per-scroll measurements out of React state so the interaction guard
|
||||||
|
// can read the current fade boundary without rerendering the sidebar.
|
||||||
|
const topFadeSizeRef = React.useRef(0);
|
||||||
|
// Update the compositor-owned mask on every scroll, but cross the React
|
||||||
|
// render boundary only when the sticky identity overlay appears or hides.
|
||||||
|
const syncTopFade = React.useCallback((scroller: HTMLElement) => {
|
||||||
|
const hasTopScroll = scroller.scrollTop > 1;
|
||||||
|
const topFadeSize = hasTopScroll
|
||||||
|
? Math.min(TOP_FADE_MIN_SIZE + scroller.scrollTop, TOP_FADE_MAX_SIZE)
|
||||||
|
: 0;
|
||||||
|
topFadeSizeRef.current = topFadeSize;
|
||||||
|
scroller.style.setProperty('--scroll-shadow-top-size', `${topFadeSize}px`);
|
||||||
|
scroller.style.setProperty(
|
||||||
|
'--scroll-shadow-top-clear-size',
|
||||||
|
`${Math.min(Math.max(topFadeSize - 8, 0), TOP_FADE_CLEAR_MAX_SIZE)}px`,
|
||||||
|
);
|
||||||
|
setHasTopScroll((prev) => (prev === hasTopScroll ? prev : hasTopScroll));
|
||||||
|
}, []);
|
||||||
|
const blockObscuredInteraction = React.useCallback((
|
||||||
|
event: React.MouseEvent<HTMLDivElement> | React.PointerEvent<HTMLDivElement>,
|
||||||
|
) => {
|
||||||
|
if ((event.target as Element).closest('[data-overlay-scrollbar-thumb], [data-sidebar-sticky-header]')) return;
|
||||||
|
const eventY = event.clientY - event.currentTarget.getBoundingClientRect().top;
|
||||||
|
if (eventY >= topFadeSizeRef.current) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
const hasProjectScroller = props.projectSections.length > 0 && props.sectionsForRender.length > 0;
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
if (enableStickyFade && hasProjectScroller && scrollContainerRef.current) {
|
||||||
|
syncTopFade(scrollContainerRef.current);
|
||||||
|
}
|
||||||
|
}, [enableStickyFade, hasProjectScroller, syncTopFade]);
|
||||||
|
let stuckProject: ProjectSection['project'] | null = null;
|
||||||
|
for (const section of props.projectSections) {
|
||||||
|
if (props.stuckProjectHeaders.has(section.project.id)) {
|
||||||
|
stuckProject = section.project;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stickyProjectLabel = stuckProject ? getProjectLabel(stuckProject, props.homeDirectory) : null;
|
||||||
|
|
||||||
if (props.sharedSessionsOnly) {
|
if (props.sharedSessionsOnly) {
|
||||||
return (
|
return (
|
||||||
@@ -144,7 +200,22 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
|||||||
// button) and holds it in place, which makes newly revealed sessions look
|
// button) and holds it in place, which makes newly revealed sessions look
|
||||||
// like they insert upward. With anchoring off, scrollTop stays put and new
|
// like they insert upward. With anchoring off, scrollTop stays put and new
|
||||||
// rows appear below naturally.
|
// rows appear below naturally.
|
||||||
<ScrollableOverlay ref={scrollContainerRef} useScrollShadow hideTopScrollShadow 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 ? '' : '')}>
|
<div
|
||||||
|
className="relative flex min-h-0 flex-1"
|
||||||
|
onPointerDownCapture={enableStickyFade ? blockObscuredInteraction : undefined}
|
||||||
|
onClickCapture={enableStickyFade ? blockObscuredInteraction : undefined}
|
||||||
|
onContextMenuCapture={enableStickyFade ? blockObscuredInteraction : undefined}
|
||||||
|
>
|
||||||
|
<ScrollableOverlay
|
||||||
|
ref={scrollContainerRef}
|
||||||
|
useScrollShadow
|
||||||
|
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 ? '' : '')}
|
||||||
|
style={enableStickyFade ? { '--scroll-shadow-top-size': '0px' } as React.CSSProperties : undefined}
|
||||||
|
onScroll={enableStickyFade ? (event) => syncTopFade(event.currentTarget) : undefined}
|
||||||
|
>
|
||||||
{props.topContent}
|
{props.topContent}
|
||||||
{props.showOnlyMainWorkspace ? (
|
{props.showOnlyMainWorkspace ? (
|
||||||
<div className="space-y-[0.6rem] py-1">
|
<div className="space-y-[0.6rem] py-1">
|
||||||
@@ -198,14 +269,9 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
|||||||
{props.sectionsForRender.map((section) => {
|
{props.sectionsForRender.map((section) => {
|
||||||
const project = section.project;
|
const project = section.project;
|
||||||
const projectKey = project.id;
|
const projectKey = project.id;
|
||||||
const projectLabel = formatProjectLabel(
|
const projectLabel = getProjectLabel(project, props.homeDirectory);
|
||||||
project.label?.trim()
|
|
||||||
|| formatDirectoryName(project.normalizedPath, props.homeDirectory)
|
|
||||||
|| project.normalizedPath,
|
|
||||||
);
|
|
||||||
const projectDescription = formatPathForDisplay(project.normalizedPath, props.homeDirectory);
|
const projectDescription = formatPathForDisplay(project.normalizedPath, props.homeDirectory);
|
||||||
const isCollapsed = props.collapsedProjects.has(projectKey);
|
const isCollapsed = props.collapsedProjects.has(projectKey);
|
||||||
const isActiveProject = projectKey === props.activeProjectId;
|
|
||||||
const isRepo = props.projectRepoStatus.get(projectKey);
|
const isRepo = props.projectRepoStatus.get(projectKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -220,10 +286,8 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
|||||||
projectIconImage={project.iconImage}
|
projectIconImage={project.iconImage}
|
||||||
projectIconBackground={project.iconBackground}
|
projectIconBackground={project.iconBackground}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
isActiveProject={isActiveProject}
|
|
||||||
isRepo={Boolean(isRepo)}
|
isRepo={Boolean(isRepo)}
|
||||||
isDesktopShell={props.isDesktopShellRuntime}
|
isDesktopShell={props.isDesktopShellRuntime}
|
||||||
isStuck={props.stuckProjectHeaders.has(projectKey)}
|
|
||||||
hideDirectoryControls={props.hideDirectoryControls}
|
hideDirectoryControls={props.hideDirectoryControls}
|
||||||
mobileVariant={props.mobileVariant}
|
mobileVariant={props.mobileVariant}
|
||||||
alwaysShowActions={props.alwaysShowActions}
|
alwaysShowActions={props.alwaysShowActions}
|
||||||
@@ -307,6 +371,31 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
)}
|
)}
|
||||||
</ScrollableOverlay>
|
</ScrollableOverlay>
|
||||||
|
{enableStickyFade && hasTopScroll && (stuckProject || props.hasSharedSessions) ? (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-x-0 top-0 z-30 flex h-7 items-center gap-1.5 pl-4 pr-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{stuckProject && stickyProjectLabel ? (
|
||||||
|
<ProjectHeaderIdentity
|
||||||
|
id={stuckProject.id}
|
||||||
|
projectLabel={stickyProjectLabel}
|
||||||
|
projectIcon={stuckProject.icon}
|
||||||
|
projectColor={stuckProject.color}
|
||||||
|
projectIconImage={stuckProject.iconImage}
|
||||||
|
projectIconBackground={stuckProject.iconBackground}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name="history" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/80" />
|
||||||
|
<span className="truncate text-[14px] font-semibold lowercase text-foreground">
|
||||||
|
{t('sessions.sidebar.activity.recentTitle')}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,78 +7,43 @@ type Args = {
|
|||||||
projectHeaderSentinelRefs: React.MutableRefObject<Map<string, HTMLDivElement | null>>;
|
projectHeaderSentinelRefs: React.MutableRefObject<Map<string, HTMLDivElement | null>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StickyHeaderArgs = {
|
|
||||||
enabled: boolean;
|
|
||||||
isDesktopShellRuntime: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useStickyHeader = (args: StickyHeaderArgs) => {
|
|
||||||
const { enabled, isDesktopShellRuntime } = args;
|
|
||||||
const [sentinel, setSentinel] = React.useState<HTMLDivElement | null>(null);
|
|
||||||
const [isStuck, setIsStuck] = React.useState(false);
|
|
||||||
|
|
||||||
const sentinelRef = React.useCallback((node: HTMLDivElement | null) => {
|
|
||||||
setSentinel(node);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!enabled || !isDesktopShellRuntime || !sentinel) {
|
|
||||||
setIsStuck(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(([entry]) => {
|
|
||||||
setIsStuck(entry.intersectionRatio < 1);
|
|
||||||
}, { threshold: 1 });
|
|
||||||
|
|
||||||
observer.observe(sentinel);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [enabled, isDesktopShellRuntime, sentinel]);
|
|
||||||
|
|
||||||
return { isStuck, sentinelRef };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useStickyProjectHeaders = (args: Args): Set<string> => {
|
export const useStickyProjectHeaders = (args: Args): Set<string> => {
|
||||||
const { enabled = true, isDesktopShellRuntime, projectSections, projectHeaderSentinelRefs } = args;
|
const { enabled = true, isDesktopShellRuntime, projectSections, projectHeaderSentinelRefs } = args;
|
||||||
const [stuckProjectHeaders, setStuckProjectHeaders] = React.useState<Set<string>>(new Set());
|
const [stuckProjectHeaders, setStuckProjectHeaders] = React.useState<Set<string>>(new Set());
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (enabled && isDesktopShellRuntime) {
|
if (!enabled || !isDesktopShellRuntime) {
|
||||||
|
setStuckProjectHeaders((prev) => (prev.size === 0 ? prev : new Set()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStuckProjectHeaders((prev) => (prev.size === 0 ? prev : new Set()));
|
const firstSentinel = Array.from(projectHeaderSentinelRefs.current.values()).find((el) => el !== null);
|
||||||
}, [enabled, isDesktopShellRuntime]);
|
const root = firstSentinel?.closest<HTMLElement>('.oc-sidebar-scroller') ?? null;
|
||||||
|
if (!root) {
|
||||||
React.useEffect(() => {
|
|
||||||
if (!enabled || !isDesktopShellRuntime) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
entries.forEach((entry) => {
|
setStuckProjectHeaders((prev) => {
|
||||||
const projectId = (entry.target as HTMLElement).dataset.projectId;
|
const next = new Set(prev);
|
||||||
if (!projectId) {
|
let changed = false;
|
||||||
return;
|
for (const entry of entries) {
|
||||||
|
const projectId = (entry.target as HTMLElement).dataset.projectId;
|
||||||
|
if (!projectId) continue;
|
||||||
|
|
||||||
|
const rootTop = entry.rootBounds?.top ?? root.getBoundingClientRect().top;
|
||||||
|
const isAboveScroller = !entry.isIntersecting && entry.boundingClientRect.top < rootTop;
|
||||||
|
if (next.has(projectId) === isAboveScroller) continue;
|
||||||
|
|
||||||
|
changed = true;
|
||||||
|
if (isAboveScroller) next.add(projectId);
|
||||||
|
else next.delete(projectId);
|
||||||
}
|
}
|
||||||
|
return changed ? next : prev;
|
||||||
setStuckProjectHeaders((prev) => {
|
|
||||||
if (!entry.isIntersecting) {
|
|
||||||
if (prev.has(projectId)) return prev;
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.add(projectId);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prev.has(projectId)) return prev;
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.delete(projectId);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ threshold: 0 },
|
{ root, threshold: 0 },
|
||||||
);
|
);
|
||||||
|
|
||||||
projectHeaderSentinelRefs.current.forEach((el) => {
|
projectHeaderSentinelRefs.current.forEach((el) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
|
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
|
||||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from '@/components/icon/Icon';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/projectMeta';
|
import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/projectMeta';
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
@@ -21,20 +21,89 @@ export type SortableDragHandleProps = {
|
|||||||
setActivatorNodeRef: ReturnType<typeof useSortable>['setActivatorNodeRef'];
|
setActivatorNodeRef: ReturnType<typeof useSortable>['setActivatorNodeRef'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SortableProjectItemProps {
|
type ProjectIdentityProps = {
|
||||||
id: string;
|
id: string;
|
||||||
disabled?: boolean;
|
|
||||||
projectLabel: string;
|
projectLabel: string;
|
||||||
projectDescription: string;
|
|
||||||
projectIcon?: string;
|
projectIcon?: string;
|
||||||
projectColor?: string;
|
projectColor?: string;
|
||||||
projectIconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' };
|
projectIconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' };
|
||||||
projectIconBackground?: string;
|
projectIconBackground?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectHeaderIdentityProps = ProjectIdentityProps & {
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
alwaysShowActions?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectHeaderIdentity: React.FC<ProjectHeaderIdentityProps> = ({
|
||||||
|
id,
|
||||||
|
projectLabel,
|
||||||
|
projectIcon,
|
||||||
|
projectColor,
|
||||||
|
projectIconImage,
|
||||||
|
projectIconBackground,
|
||||||
|
isCollapsed,
|
||||||
|
alwaysShowActions = false,
|
||||||
|
}) => {
|
||||||
|
const { currentTheme } = useThemeSystem();
|
||||||
|
const projectIconName = projectIcon ? PROJECT_ICON_MAP[projectIcon] : null;
|
||||||
|
const iconColor = projectColor ? (PROJECT_COLOR_MAP[projectColor] ?? null) : null;
|
||||||
|
const hasCollapseControl = isCollapsed !== undefined;
|
||||||
|
const iconVisibilityClassName = hasCollapseControl
|
||||||
|
? (alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="inline-flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
||||||
|
{hasCollapseControl ? (
|
||||||
|
<span className={cn(
|
||||||
|
'h-3.5 w-3.5 items-center justify-center text-muted-foreground',
|
||||||
|
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/project:inline-flex group-focus-within/project:inline-flex',
|
||||||
|
)}>
|
||||||
|
<Icon name={isCollapsed ? 'arrow-right-s' : 'arrow-down-s'} className="h-3.5 w-3.5" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{projectIconImage ? (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'h-3.5 w-3.5 items-center justify-center overflow-hidden rounded-[3px]',
|
||||||
|
hasCollapseControl && alwaysShowActions ? 'hidden' : 'inline-flex',
|
||||||
|
iconVisibilityClassName,
|
||||||
|
)}
|
||||||
|
style={projectIconBackground ? { backgroundColor: projectIconBackground } : undefined}
|
||||||
|
>
|
||||||
|
<ProjectIconImage
|
||||||
|
project={{ id, iconImage: projectIconImage }}
|
||||||
|
options={{
|
||||||
|
themeVariant: currentTheme.metadata.variant,
|
||||||
|
iconColor: currentTheme.colors.surface.foreground,
|
||||||
|
}}
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
fallback={projectIconName ? (
|
||||||
|
<Icon name={projectIconName} className="h-3.5 w-3.5" style={iconColor ? { color: iconColor } : undefined} />
|
||||||
|
) : (
|
||||||
|
<Icon name="folder" className="h-3.5 w-3.5 text-muted-foreground/80" style={iconColor ? { color: iconColor } : undefined} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : projectIconName ? (
|
||||||
|
<Icon name={projectIconName} className={cn('h-3.5 w-3.5', iconVisibilityClassName)} style={iconColor ? { color: iconColor } : undefined} />
|
||||||
|
) : (
|
||||||
|
<Icon name="folder" className={cn('h-3.5 w-3.5 text-muted-foreground/80', iconVisibilityClassName)} style={iconColor ? { color: iconColor } : undefined} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="truncate text-[14px] font-semibold lowercase text-foreground">{projectLabel}</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SortableProjectItemProps extends ProjectIdentityProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
projectDescription: string;
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
isActiveProject: boolean;
|
|
||||||
isRepo: boolean;
|
isRepo: boolean;
|
||||||
isDesktopShell: boolean;
|
isDesktopShell: boolean;
|
||||||
isStuck: boolean;
|
|
||||||
hideDirectoryControls: boolean;
|
hideDirectoryControls: boolean;
|
||||||
mobileVariant: boolean;
|
mobileVariant: boolean;
|
||||||
alwaysShowActions: boolean;
|
alwaysShowActions: boolean;
|
||||||
@@ -64,10 +133,8 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
projectIconImage,
|
projectIconImage,
|
||||||
projectIconBackground,
|
projectIconBackground,
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
isActiveProject,
|
|
||||||
isRepo,
|
isRepo,
|
||||||
isDesktopShell,
|
isDesktopShell,
|
||||||
isStuck,
|
|
||||||
hideDirectoryControls,
|
hideDirectoryControls,
|
||||||
alwaysShowActions,
|
alwaysShowActions,
|
||||||
onToggle,
|
onToggle,
|
||||||
@@ -85,7 +152,6 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
statusIndicator = null,
|
statusIndicator = null,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { currentTheme } = useThemeSystem();
|
|
||||||
const stickyZoneHeaders = useSessionDisplayStore((state) => state.stickyZoneHeaders);
|
const stickyZoneHeaders = useSessionDisplayStore((state) => state.stickyZoneHeaders);
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
@@ -101,9 +167,6 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
const isMenuOpen = openSidebarMenuKey === menuInstanceKey;
|
const isMenuOpen = openSidebarMenuKey === menuInstanceKey;
|
||||||
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false);
|
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false);
|
||||||
|
|
||||||
const projectIconName = projectIcon ? PROJECT_ICON_MAP[projectIcon] : null;
|
|
||||||
const iconColor = projectColor ? (PROJECT_COLOR_MAP[projectColor] ?? null) : null;
|
|
||||||
|
|
||||||
const handleMenuOpenChange = React.useCallback((open: boolean) => {
|
const handleMenuOpenChange = React.useCallback((open: boolean) => {
|
||||||
if (open) setIsContextMenuOpen(false);
|
if (open) setIsContextMenuOpen(false);
|
||||||
setOpenSidebarMenuKey(open ? menuInstanceKey : null);
|
setOpenSidebarMenuKey(open ? menuInstanceKey : null);
|
||||||
@@ -187,9 +250,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
render={
|
render={
|
||||||
// Sticky zone header: this trigger div is a direct child of
|
// Sticky zone header: this trigger div is a direct child of
|
||||||
// the project wrapper (which spans header + sessions), so it
|
// the project wrapper (which spans header + sessions), so it
|
||||||
// can stick for the whole zone. The solid sidebar backing
|
// can stick for the whole zone.
|
||||||
// keeps scrolled session rows from showing through the
|
|
||||||
// translucent band.
|
|
||||||
// Full-bleed band: pull past the list container's padding so
|
// Full-bleed band: pull past the list container's padding so
|
||||||
// the section band spans the entire sidebar width (ref: edge-
|
// the section band spans the entire sidebar width (ref: edge-
|
||||||
// to-edge section headers, not rounded pills).
|
// to-edge section headers, not rounded pills).
|
||||||
@@ -197,8 +258,8 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'-ml-2.5 -mr-2 text-left group/project select-none',
|
'-ml-2.5 -mr-2 text-left group/project select-none',
|
||||||
stickyZoneHeaders && 'sticky top-0 z-20 bg-sidebar',
|
stickyZoneHeaders && 'sticky top-0 z-20 bg-sidebar',
|
||||||
stickyZoneHeaders && isStuck && 'oc-zone-header-backing',
|
|
||||||
)}
|
)}
|
||||||
|
data-sidebar-sticky-header={stickyZoneHeaders ? 'true' : undefined}
|
||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
// VS Code hides project actions entirely (hideDirectoryControls).
|
// VS Code hides project actions entirely (hideDirectoryControls).
|
||||||
if (hideDirectoryControls) return;
|
if (hideDirectoryControls) return;
|
||||||
@@ -209,14 +270,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className="relative flex items-center gap-1 py-1 pl-4 pr-3.5"
|
||||||
// pl-4 keeps the icon/text aligned with the padded rows below
|
|
||||||
// (container pl-2.5 + band px-1.5 it replaces).
|
|
||||||
'relative flex items-center gap-1 py-1 pl-4 pr-3.5',
|
|
||||||
// Desktop shell reports when the header is actually stuck;
|
|
||||||
// a subtle elevation makes the pinned state readable.
|
|
||||||
isStuck && 'shadow-md',
|
|
||||||
)}
|
|
||||||
{...attributes}
|
{...attributes}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -233,47 +287,16 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
: (alwaysShowActions ? 'pr-14' : 'pr-7 group-hover/project:pr-14 group-focus-within/project:pr-14'),
|
: (alwaysShowActions ? 'pr-14' : 'pr-7 group-hover/project:pr-14 group-focus-within/project:pr-14'),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="inline-flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
<ProjectHeaderIdentity
|
||||||
<span className={cn(
|
id={id}
|
||||||
'h-3.5 w-3.5 items-center justify-center text-muted-foreground',
|
projectLabel={projectLabel}
|
||||||
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/project:inline-flex group-focus-within/project:inline-flex',
|
projectIcon={projectIcon}
|
||||||
)}>
|
projectColor={projectColor}
|
||||||
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
|
projectIconImage={projectIconImage}
|
||||||
</span>
|
projectIconBackground={projectIconBackground}
|
||||||
{projectIconImage ? (
|
isCollapsed={isCollapsed}
|
||||||
<span
|
alwaysShowActions={alwaysShowActions}
|
||||||
className={cn(
|
/>
|
||||||
'h-3.5 w-3.5 items-center justify-center overflow-hidden rounded-[3px]',
|
|
||||||
alwaysShowActions ? 'hidden' : 'inline-flex group-hover/project:hidden group-focus-within/project:hidden',
|
|
||||||
)}
|
|
||||||
style={projectIconBackground ? { backgroundColor: projectIconBackground } : undefined}
|
|
||||||
>
|
|
||||||
<ProjectIconImage
|
|
||||||
project={{ id, iconImage: projectIconImage }}
|
|
||||||
options={{
|
|
||||||
themeVariant: currentTheme.metadata.variant,
|
|
||||||
iconColor: currentTheme.colors.surface.foreground,
|
|
||||||
}}
|
|
||||||
className="h-full w-full object-contain"
|
|
||||||
fallback={projectIconName ? (
|
|
||||||
<Icon name={projectIconName} className="h-3.5 w-3.5" style={iconColor ? { color: iconColor } : undefined} />
|
|
||||||
) : (
|
|
||||||
<Icon name="folder" className="h-3.5 w-3.5 text-muted-foreground/80" style={iconColor ? { color: iconColor } : undefined} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : projectIconName ? (
|
|
||||||
<Icon name={projectIconName} className={cn('h-3.5 w-3.5', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
|
|
||||||
) : (
|
|
||||||
<Icon name="folder" className={cn('h-3.5 w-3.5 text-muted-foreground/80', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className={cn(
|
|
||||||
'text-[14px] font-semibold truncate lowercase',
|
|
||||||
isActiveProject ? 'text-foreground' : 'text-foreground group-hover/project:text-foreground',
|
|
||||||
)}>
|
|
||||||
{projectLabel}
|
|
||||||
</span>
|
|
||||||
{statusIndicator ? (
|
{statusIndicator ? (
|
||||||
<span className="ml-1 inline-flex flex-shrink-0 items-center">{statusIndicator}</span>
|
<span className="ml-1 inline-flex flex-shrink-0 items-center">{statusIndicator}</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -316,14 +316,16 @@ div[data-chat-input-footer="true"] {
|
|||||||
mask-image: linear-gradient(
|
mask-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent 0%,
|
transparent 0%,
|
||||||
#000 var(--scroll-shadow-size),
|
transparent var(--scroll-shadow-top-clear-size, 0px),
|
||||||
|
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
|
||||||
#000 calc(100% - var(--scroll-shadow-size)),
|
#000 calc(100% - var(--scroll-shadow-size)),
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
-webkit-mask-image: linear-gradient(
|
-webkit-mask-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent 0%,
|
transparent 0%,
|
||||||
#000 var(--scroll-shadow-size),
|
transparent var(--scroll-shadow-top-clear-size, 0px),
|
||||||
|
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
|
||||||
#000 calc(100% - var(--scroll-shadow-size)),
|
#000 calc(100% - var(--scroll-shadow-size)),
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
@@ -333,13 +335,15 @@ div[data-chat-input-footer="true"] {
|
|||||||
mask-image: linear-gradient(
|
mask-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent 0%,
|
transparent 0%,
|
||||||
#000 var(--scroll-shadow-size),
|
transparent var(--scroll-shadow-top-clear-size, 0px),
|
||||||
|
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
|
||||||
#000 100%
|
#000 100%
|
||||||
);
|
);
|
||||||
-webkit-mask-image: linear-gradient(
|
-webkit-mask-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent 0%,
|
transparent 0%,
|
||||||
#000 var(--scroll-shadow-size),
|
transparent var(--scroll-shadow-top-clear-size, 0px),
|
||||||
|
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
|
||||||
#000 100%
|
#000 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,6 @@
|
|||||||
--sidebar-accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
--sidebar-accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||||
--sidebar-border: oklch(0.85 0.02 70); /* Warm border */
|
--sidebar-border: oklch(0.85 0.02 70); /* Warm border */
|
||||||
--sidebar-ring: oklch(0.65 0.2 55); /* Focus ring */
|
--sidebar-ring: oklch(0.65 0.2 55); /* Focus ring */
|
||||||
--sidebar-stuck-bg: var(--sidebar); /* Desktop sidebar sticky header background (match sidebar) */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -97,7 +96,6 @@
|
|||||||
--sidebar-accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
--sidebar-accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||||
--sidebar-border: oklch(0.31 0.01 35); /* #393836 */
|
--sidebar-border: oklch(0.31 0.01 35); /* #393836 */
|
||||||
--sidebar-ring: oklch(0.77 0.17 85); /* #edb449 */
|
--sidebar-ring: oklch(0.77 0.17 85); /* #edb449 */
|
||||||
--sidebar-stuck-bg: var(--sidebar); /* Desktop sidebar sticky header background (match sidebar) */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -142,14 +140,6 @@
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stuck sidebar zone headers must fully mask the rows scrolling under
|
|
||||||
them. Electron's transparent/vibrancy windows break backdrop-filter
|
|
||||||
(electron#20357), so use the opaque sidebar tone instead. */
|
|
||||||
:root.desktop-runtime[data-oc-vibrancy] .oc-zone-header-backing {
|
|
||||||
background: var(--sidebar-stuck-bg) !important;
|
|
||||||
background-color: var(--sidebar-stuck-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-sans {
|
.font-sans {
|
||||||
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) !important;
|
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user