feat(frontend): replace native scrollbar styling with OverlayScrollbars
Adopt OverlayScrollbars for cross-platform consistent overlay scrollbars with auto-hide on pointer leave, hover grow effect, and click-to-scroll. - Add overlayscrollbars + overlayscrollbars-react dependencies - Rewrite ScrollArea component from Radix to OverlayScrollbars wrapper - Define custom theme `os-theme-codeg` in globals.css (6px → 8px on hover) - Initialize body-level overlay scrollbar via OverlayScrollbarsInit - Migrate all scrollbar-thin / scrollbar-thin-edge usages to ScrollArea - Keep native .scrollbar-thin fallback for virtua scroll containers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,8 @@
|
|||||||
"next": "^16",
|
"next": "^16",
|
||||||
"next-intl": "^4.8.3",
|
"next-intl": "^4.8.3",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"overlayscrollbars": "^2.15.1",
|
||||||
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -107,6 +107,12 @@ importers:
|
|||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
overlayscrollbars:
|
||||||
|
specifier: ^2.15.1
|
||||||
|
version: 2.15.1
|
||||||
|
overlayscrollbars-react:
|
||||||
|
specifier: ^0.5.6
|
||||||
|
version: 0.5.6(overlayscrollbars@2.15.1)(react@19.2.4)
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.5.6
|
specifier: ^8.5.6
|
||||||
version: 8.5.6
|
version: 8.5.6
|
||||||
@@ -5227,6 +5233,15 @@ packages:
|
|||||||
outvariant@1.4.3:
|
outvariant@1.4.3:
|
||||||
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
|
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
|
||||||
|
|
||||||
|
overlayscrollbars-react@0.5.6:
|
||||||
|
resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==}
|
||||||
|
peerDependencies:
|
||||||
|
overlayscrollbars: ^2.0.0
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
|
overlayscrollbars@2.15.1:
|
||||||
|
resolution: {integrity: sha512-glX26JwjL+Tkzv0JNOWdW4VozP5dGXO+Wx8+TPrdTEJTSYT/8eJS8yXM+fewjU0nFq/JeCa+X+BqABNjC4YZSA==}
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -12402,6 +12417,13 @@ snapshots:
|
|||||||
|
|
||||||
outvariant@1.4.3: {}
|
outvariant@1.4.3: {}
|
||||||
|
|
||||||
|
overlayscrollbars-react@0.5.6(overlayscrollbars@2.15.1)(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
overlayscrollbars: 2.15.1
|
||||||
|
react: 19.2.4
|
||||||
|
|
||||||
|
overlayscrollbars@2.15.1: {}
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.3.0
|
get-intrinsic: 1.3.0
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "shadcn/tailwind.css";
|
@import "shadcn/tailwind.css";
|
||||||
@import "@xterm/xterm/css/xterm.css";
|
@import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -1002,14 +1001,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Unified scrollbar style for scrollable containers.
|
/* Native fallback for containers that cannot use OverlayScrollbars (e.g. virtua) */
|
||||||
Thin overlay scrollbar — no gutter reserved, no layout shift. */
|
.scrollbar-thin {
|
||||||
.scrollbar-thin,
|
|
||||||
.scrollbar-thin-edge {
|
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--border) transparent;
|
scrollbar-color: var(--border) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* OverlayScrollbars custom theme */
|
||||||
|
.os-theme-codeg {
|
||||||
|
--os-size: 6px;
|
||||||
|
--os-handle-bg: var(--border);
|
||||||
|
--os-handle-bg-hover: var(--muted-foreground);
|
||||||
|
--os-handle-bg-active: var(--muted-foreground);
|
||||||
|
--os-handle-border-radius: 999px;
|
||||||
|
--os-handle-perpendicular-size: 100%;
|
||||||
|
--os-handle-perpendicular-size-hover: 100%;
|
||||||
|
--os-handle-perpendicular-size-active: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-theme-codeg:hover {
|
||||||
|
--os-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Streamdown code blocks: dark mode via shiki dual-theme CSS variables */
|
/* Streamdown code blocks: dark mode via shiki dual-theme CSS variables */
|
||||||
.dark [data-streamdown="code-block-body"] {
|
.dark [data-streamdown="code-block-body"] {
|
||||||
background-color: var(--shiki-dark-bg, var(--sdm-bg, transparent)) !important;
|
background-color: var(--shiki-dark-bg, var(--sdm-bg, transparent)) !important;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ThemeProvider } from "@/components/theme-provider"
|
|||||||
import { toIntlLocale } from "@/lib/i18n"
|
import { toIntlLocale } from "@/lib/i18n"
|
||||||
import { APPEARANCE_INIT_SCRIPT } from "@/lib/appearance-script"
|
import { APPEARANCE_INIT_SCRIPT } from "@/lib/appearance-script"
|
||||||
import { AppearanceProvider } from "@/components/appearance-provider"
|
import { AppearanceProvider } from "@/components/appearance-provider"
|
||||||
|
import { OverlayScrollbarsInit } from "@/components/overlay-scrollbars-init"
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -68,7 +69,10 @@ export default async function RootLayout({
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<AppearanceProvider>{children}</AppearanceProvider>
|
<AppearanceProvider>
|
||||||
|
<OverlayScrollbarsInit />
|
||||||
|
{children}
|
||||||
|
</AppearanceProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</AppI18nProvider>
|
</AppI18nProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { STATUS_ORDER, STATUS_COLORS } from "@/lib/types"
|
|||||||
import { SidebarConversationCard } from "./sidebar-conversation-card"
|
import { SidebarConversationCard } from "./sidebar-conversation-card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
@@ -205,8 +206,6 @@ export function SidebarConversationList({
|
|||||||
cancelled: false,
|
cancelled: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const scrollToActiveRef = useRef<() => void>(() => {})
|
const scrollToActiveRef = useRef<() => void>(() => {})
|
||||||
const pendingScrollRef = useRef(false)
|
const pendingScrollRef = useRef(false)
|
||||||
const virtualizerRef = useRef<VirtualizerHandle>(null)
|
const virtualizerRef = useRef<VirtualizerHandle>(null)
|
||||||
@@ -479,12 +478,8 @@ export function SidebarConversationList({
|
|||||||
) : (
|
) : (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<div
|
<ScrollArea
|
||||||
ref={scrollContainerRef}
|
className={cn("flex-1 min-h-0 px-2", "[overflow-anchor:none]")}
|
||||||
className={cn(
|
|
||||||
"flex-1 min-h-0 overflow-y-auto scrollbar-thin px-2",
|
|
||||||
"[overflow-anchor:none]"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Virtualizer ref={virtualizerRef} itemSize={CARD_HEIGHT}>
|
<Virtualizer ref={virtualizerRef} itemSize={CARD_HEIGHT}>
|
||||||
{flatItems.map((item) => {
|
{flatItems.map((item) => {
|
||||||
@@ -537,7 +532,7 @@ export function SidebarConversationList({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Virtualizer>
|
</Virtualizer>
|
||||||
</div>
|
</ScrollArea>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem onSelect={handleNewConversation}>
|
<ContextMenuItem onSelect={handleNewConversation}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useMemo } from "react"
|
|||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useFolderContext } from "@/contexts/folder-context"
|
import { useFolderContext } from "@/contexts/folder-context"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
|
||||||
type RowMarker = "none" | "added" | "deleted" | "modified"
|
type RowMarker = "none" | "added" | "deleted" | "modified"
|
||||||
type DiffFileMode = "modified" | "added" | "deleted" | "renamed"
|
type DiffFileMode = "modified" | "added" | "deleted" | "renamed"
|
||||||
@@ -546,16 +547,16 @@ export function UnifiedDiffPreview({
|
|||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("h-full overflow-auto scrollbar-thin", className)}>
|
<ScrollArea className={cn("h-full", className)} x="scroll">
|
||||||
<pre className="font-mono text-[11px] leading-5 whitespace-pre-wrap text-muted-foreground p-3">
|
<pre className="font-mono text-[11px] leading-5 whitespace-pre-wrap text-muted-foreground p-3">
|
||||||
{diffText}
|
{diffText}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("h-full overflow-auto scrollbar-thin", className)}>
|
<ScrollArea className={cn("h-full", className)} x="scroll">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{files.map((file) => {
|
{files.map((file) => {
|
||||||
const newFile = isNewFileOnly(file)
|
const newFile = isNewFileOnly(file)
|
||||||
@@ -586,7 +587,7 @@ export function UnifiedDiffPreview({
|
|||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="overflow-auto scrollbar-thin">
|
<ScrollArea x="scroll">
|
||||||
<div className="inline-block min-w-full">
|
<div className="inline-block min-w-full">
|
||||||
{newFile
|
{newFile
|
||||||
? file.hunks.map((hunk) => (
|
? file.hunks.map((hunk) => (
|
||||||
@@ -599,11 +600,11 @@ export function UnifiedDiffPreview({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { Streamdown } from "streamdown"
|
|||||||
import { readFileBase64 } from "@/lib/api"
|
import { readFileBase64 } from "@/lib/api"
|
||||||
import { normalizeMathDelimiters } from "@/components/ai-elements/message"
|
import { normalizeMathDelimiters } from "@/components/ai-elements/message"
|
||||||
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
|
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import "@/lib/monaco-local"
|
import "@/lib/monaco-local"
|
||||||
|
|
||||||
const math = createMathPlugin({ singleDollarTextMath: true })
|
const math = createMathPlugin({ singleDollarTextMath: true })
|
||||||
@@ -693,7 +694,7 @@ function DiffFileList({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin">
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{diffOutline.files.map((file) => (
|
{diffOutline.files.map((file) => (
|
||||||
<ContextMenu key={file.key}>
|
<ContextMenu key={file.key}>
|
||||||
@@ -743,7 +744,7 @@ function DiffFileList({
|
|||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
stopFileTreeWatch,
|
stopFileTreeWatch,
|
||||||
} from "@/lib/api"
|
} from "@/lib/api"
|
||||||
import { emitAttachFileToSession } from "@/lib/session-attachment-events"
|
import { emitAttachFileToSession } from "@/lib/session-attachment-events"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import type {
|
import type {
|
||||||
FileTreeChangedEvent,
|
FileTreeChangedEvent,
|
||||||
FileTreeNode,
|
FileTreeNode,
|
||||||
@@ -2167,7 +2168,7 @@ export function FileTreeTab() {
|
|||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<div className="flex-1 min-h-0 overflow-auto pb-1 scrollbar-thin-edge">
|
<ScrollArea className="flex-1 min-h-0 pb-1" x="scroll">
|
||||||
<FileTree
|
<FileTree
|
||||||
key={folder?.path ?? "file-tree-empty"}
|
key={folder?.path ?? "file-tree-empty"}
|
||||||
className="border-0 rounded-none bg-transparent w-max min-w-full"
|
className="border-0 rounded-none bg-transparent w-max min-w-full"
|
||||||
@@ -2319,7 +2320,7 @@ export function FileTreeTab() {
|
|||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
)}
|
)}
|
||||||
</FileTree>
|
</FileTree>
|
||||||
</div>
|
</ScrollArea>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuSub>
|
<ContextMenuSub>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
FileTreeFolder,
|
FileTreeFolder,
|
||||||
} from "@/components/ai-elements/file-tree"
|
} from "@/components/ai-elements/file-tree"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
@@ -1287,7 +1288,7 @@ export function GitChangesTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-full min-h-0 overflow-y-auto scrollbar-thin-edge">
|
<ScrollArea className="h-full min-h-0" x="scroll">
|
||||||
{trackedChanges.length === 0 && untrackedChanges.length === 0 ? (
|
{trackedChanges.length === 0 && untrackedChanges.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-full p-4">
|
<div className="flex items-center justify-center h-full p-4">
|
||||||
<p className="text-xs text-muted-foreground text-center">
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
@@ -1506,7 +1507,7 @@ export function GitChangesTab() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ScrollArea>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={Boolean(directoryGitActionType && directoryGitActionTarget)}
|
open={Boolean(directoryGitActionType && directoryGitActionTarget)}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type ReactElement,
|
type ReactElement,
|
||||||
type UIEvent,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -89,6 +88,7 @@ import {
|
|||||||
import type { GitBranchList, GitLogEntry, GitLogFileChange } from "@/lib/types"
|
import type { GitBranchList, GitLogEntry, GitLogFileChange } from "@/lib/types"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { toErrorMessage } from "@/lib/app-error"
|
import { toErrorMessage } from "@/lib/app-error"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
|
||||||
function formatRelativeTime(
|
function formatRelativeTime(
|
||||||
dateStr: string,
|
dateStr: string,
|
||||||
@@ -896,14 +896,15 @@ export function GitLogTab() {
|
|||||||
}
|
}
|
||||||
}, [folder, refreshBranches, fetchLog])
|
}, [folder, refreshBranches, fetchLog])
|
||||||
|
|
||||||
const handleScroll = useCallback((e: UIEvent<HTMLDivElement>) => {
|
const handleScroll = useCallback((e: Event) => {
|
||||||
const nextScrolled = e.currentTarget.scrollTop > 0
|
const target = e.target as HTMLElement
|
||||||
|
const nextScrolled = target.scrollTop > 0
|
||||||
setScrolled((prev) => (prev === nextScrolled ? prev : nextScrolled))
|
setScrolled((prev) => (prev === nextScrolled ? prev : nextScrolled))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full overflow-y-auto scrollbar-thin px-3 py-3">
|
<ScrollArea className="h-full px-3 py-3">
|
||||||
{hasBranches && (
|
{hasBranches && (
|
||||||
<BranchSelector
|
<BranchSelector
|
||||||
branchList={branchList}
|
branchList={branchList}
|
||||||
@@ -923,13 +924,13 @@ export function GitLogTab() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full overflow-y-auto scrollbar-thin px-3 py-3">
|
<ScrollArea className="h-full px-3 py-3">
|
||||||
{hasBranches && (
|
{hasBranches && (
|
||||||
<BranchSelector
|
<BranchSelector
|
||||||
branchList={branchList}
|
branchList={branchList}
|
||||||
@@ -953,13 +954,14 @@ export function GitLogTab() {
|
|||||||
{t("retry")}
|
{t("retry")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full overflow-y-auto scrollbar-thin px-3 py-3">
|
<ScrollArea className="h-full px-3 py-3">
|
||||||
|
<div className="flex flex-col min-h-full">
|
||||||
{hasBranches && (
|
{hasBranches && (
|
||||||
<BranchSelector
|
<BranchSelector
|
||||||
branchList={branchList}
|
branchList={branchList}
|
||||||
@@ -976,6 +978,7 @@ export function GitLogTab() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,10 +986,11 @@ export function GitLogTab() {
|
|||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<div
|
<ScrollArea
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="flex-1 min-h-0 overflow-y-auto scrollbar-thin px-3 py-3 space-y-3"
|
className="flex-1 min-h-0 px-3 py-3"
|
||||||
>
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
{hasBranches && (
|
{hasBranches && (
|
||||||
<div
|
<div
|
||||||
className={`sticky top-0 z-10 rounded-full bg-sidebar/85 supports-[backdrop-filter]:bg-sidebar/70 backdrop-blur ${scrolled ? "p-2 shadow-md" : "p-0"}`}
|
className={`sticky top-0 z-10 rounded-full bg-sidebar/85 supports-[backdrop-filter]:bg-sidebar/70 backdrop-blur ${scrolled ? "p-2 shadow-md" : "p-0"}`}
|
||||||
@@ -1233,6 +1237,7 @@ export function GitLogTab() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible"
|
} from "@/components/ui/collapsible"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function isRemovedFileDiff(diff: string | null): boolean {
|
function isRemovedFileDiff(diff: string | null): boolean {
|
||||||
@@ -290,9 +291,9 @@ export function SessionFilesTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin px-2">
|
<ScrollArea className="flex-1 min-h-0 px-2">
|
||||||
<SessionFilesContent conversationId={conversationId} />
|
<SessionFilesContent conversationId={conversationId} />
|
||||||
</div>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/components/overlay-scrollbars-init.tsx
Normal file
25
src/components/overlay-scrollbars-init.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import "overlayscrollbars/overlayscrollbars.css"
|
||||||
|
import { useOverlayScrollbars } from "overlayscrollbars-react"
|
||||||
|
|
||||||
|
export function OverlayScrollbarsInit() {
|
||||||
|
const [init] = useOverlayScrollbars({
|
||||||
|
options: {
|
||||||
|
scrollbars: {
|
||||||
|
theme: "os-theme-codeg",
|
||||||
|
autoHide: "leave",
|
||||||
|
clickScroll: true,
|
||||||
|
},
|
||||||
|
overflow: { x: "hidden" },
|
||||||
|
},
|
||||||
|
defer: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
init(document.body)
|
||||||
|
}, [init])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,55 +1,59 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import { useMemo } from "react"
|
||||||
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
import {
|
||||||
|
OverlayScrollbarsComponent,
|
||||||
|
type OverlayScrollbarsComponentRef,
|
||||||
|
} from "overlayscrollbars-react"
|
||||||
|
import type { OverlayScrollbarsComponentProps } from "overlayscrollbars-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
type ScrollAreaProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
x?: "scroll" | "hidden"
|
||||||
|
y?: "scroll" | "hidden"
|
||||||
|
onScroll?: (event: Event) => void
|
||||||
|
ref?: React.Ref<OverlayScrollbarsComponentRef>
|
||||||
|
}
|
||||||
|
|
||||||
function ScrollArea({
|
const BASE_OPTIONS: OverlayScrollbarsComponentProps["options"] = {
|
||||||
className,
|
scrollbars: {
|
||||||
|
theme: "os-theme-codeg",
|
||||||
|
autoHide: "leave",
|
||||||
|
clickScroll: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScrollArea({
|
||||||
children,
|
children,
|
||||||
...props
|
className,
|
||||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
x = "hidden",
|
||||||
|
y = "scroll",
|
||||||
|
onScroll,
|
||||||
|
ref,
|
||||||
|
}: ScrollAreaProps) {
|
||||||
|
const options = useMemo<OverlayScrollbarsComponentProps["options"]>(
|
||||||
|
() => ({
|
||||||
|
...BASE_OPTIONS,
|
||||||
|
overflow: { x, y },
|
||||||
|
}),
|
||||||
|
[x, y]
|
||||||
|
)
|
||||||
|
|
||||||
|
const events = useMemo<OverlayScrollbarsComponentProps["events"]>(
|
||||||
|
() => (onScroll ? { scroll: (_instance, event) => onScroll(event) } : {}),
|
||||||
|
[onScroll]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollAreaPrimitive.Root
|
<OverlayScrollbarsComponent
|
||||||
data-slot="scroll-area"
|
ref={ref}
|
||||||
className={cn("relative", className)}
|
className={className}
|
||||||
{...props}
|
options={options}
|
||||||
>
|
events={events}
|
||||||
<ScrollAreaPrimitive.Viewport
|
defer
|
||||||
data-slot="scroll-area-viewport"
|
|
||||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</OverlayScrollbarsComponent>
|
||||||
<ScrollBar />
|
|
||||||
<ScrollAreaPrimitive.Corner />
|
|
||||||
</ScrollAreaPrimitive.Root>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScrollBar({
|
|
||||||
className,
|
|
||||||
orientation = "vertical",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
|
||||||
return (
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
||||||
data-slot="scroll-area-scrollbar"
|
|
||||||
data-orientation={orientation}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
|
||||||
data-slot="scroll-area-thumb"
|
|
||||||
className="rounded-full bg-border relative flex-1"
|
|
||||||
/>
|
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
|
||||||
|
|||||||
Reference in New Issue
Block a user