Skip to content

Commit fe89bed

Browse files
committed
wip(app): custom scroll view
1 parent 1e48d7f commit fe89bed

12 files changed

Lines changed: 345 additions & 89 deletions

File tree

packages/app/e2e/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export async function hoverSessionItem(page: Page, sessionID: string) {
225225
export async function openSessionMoreMenu(page: Page, sessionID: string) {
226226
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
227227

228-
const scroller = page.locator(".session-scroller").first()
228+
const scroller = page.locator(".scroll-view__viewport").first()
229229
await expect(scroller).toBeVisible()
230230
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
231231

packages/app/e2e/session/session.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession }
4444
const menu = await openSessionMoreMenu(page, session.id)
4545
await clickMenuItem(menu, /rename/i)
4646

47-
const input = page.locator(".session-scroller").locator(inlineInputSelector).first()
47+
const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first()
4848
await expect(input).toBeVisible()
4949
await expect(input).toBeFocused()
5050
await input.fill(renamedTitle)

packages/app/src/components/session/session-context-tab.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Accordion } from "@opencode-ai/ui/accordion"
1111
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
1212
import { Code } from "@opencode-ai/ui/code"
1313
import { Markdown } from "@opencode-ai/ui/markdown"
14+
import { ScrollView } from "@opencode-ai/ui/scroll-view"
1415
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
1516
import { useLanguage } from "@/context/language"
1617
import { getSessionContextMetrics } from "./session-context-metrics"
@@ -268,9 +269,9 @@ export function SessionContextTab() {
268269
})
269270

270271
return (
271-
<div
272-
class="@container h-full overflow-y-auto no-scrollbar pb-10"
273-
ref={(el) => {
272+
<ScrollView
273+
class="@container h-full pb-10"
274+
viewportRef={(el) => {
274275
scroll = el
275276
restoreScroll()
276277
}}
@@ -336,6 +337,6 @@ export function SessionContextTab() {
336337
</Accordion>
337338
</div>
338339
</div>
339-
</div>
340+
</ScrollView>
340341
)
341342
}

packages/app/src/pages/session/composer/session-question-dock.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
6262
const measure = () => {
6363
if (!root) return
6464

65-
const scroller = document.querySelector(".session-scroller")
65+
const scroller = document.querySelector(".scroll-view__viewport")
6666
const head = scroller instanceof HTMLElement ? scroller.firstElementChild : undefined
6767
const top =
6868
head instanceof HTMLElement && head.classList.contains("sticky") ? head.getBoundingClientRect().bottom : 0
@@ -95,7 +95,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
9595
window.addEventListener("resize", update)
9696

9797
const dock = root?.closest('[data-component="session-prompt-dock"]')
98-
const scroller = document.querySelector(".session-scroller")
98+
const scroller = document.querySelector(".scroll-view__viewport")
9999
const observer = new ResizeObserver(update)
100100
if (dock instanceof HTMLElement) observer.observe(dock)
101101
if (scroller instanceof HTMLElement) observer.observe(scroller)

packages/app/src/pages/session/file-tabs.tsx

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { showToast } from "@opencode-ai/ui/toast"
99
import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment"
1010
import { Mark } from "@opencode-ai/ui/logo"
1111
import { Tabs } from "@opencode-ai/ui/tabs"
12+
import { ScrollView } from "@opencode-ai/ui/scroll-view"
1213
import { useLayout } from "@/context/layout"
1314
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
1415
import { useComments } from "@/context/comments"
@@ -509,51 +510,52 @@ export function FileTabContent(props: { tab: string }) {
509510
)
510511

511512
return (
512-
<Tabs.Content
513-
value={props.tab}
514-
class="mt-3 relative"
515-
ref={(el: HTMLDivElement) => {
516-
scroll = el
517-
restoreScroll()
518-
}}
519-
onScroll={handleScroll}
520-
>
521-
<Switch>
522-
<Match when={state()?.loaded && isImage()}>
523-
<div class="px-6 py-4 pb-40">
524-
<img
525-
src={imageDataUrl()}
526-
alt={path()}
527-
class="max-w-full"
528-
onLoad={() => requestAnimationFrame(restoreScroll)}
529-
/>
530-
</div>
531-
</Match>
532-
<Match when={state()?.loaded && isSvg()}>
533-
<div class="flex flex-col gap-4 px-6 py-4">
534-
{renderCode(svgContent() ?? "", "")}
535-
<Show when={svgPreviewUrl()}>
536-
<div class="flex justify-center pb-40">
537-
<img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
513+
<Tabs.Content value={props.tab} class="mt-3 relative h-full">
514+
<ScrollView
515+
class="h-full"
516+
viewportRef={(el: HTMLDivElement) => {
517+
scroll = el
518+
restoreScroll()
519+
}}
520+
onScroll={handleScroll as any}
521+
>
522+
<Switch>
523+
<Match when={state()?.loaded && isImage()}>
524+
<div class="px-6 py-4 pb-40">
525+
<img
526+
src={imageDataUrl()}
527+
alt={path()}
528+
class="max-w-full"
529+
onLoad={() => requestAnimationFrame(restoreScroll)}
530+
/>
531+
</div>
532+
</Match>
533+
<Match when={state()?.loaded && isSvg()}>
534+
<div class="flex flex-col gap-4 px-6 py-4">
535+
{renderCode(svgContent() ?? "", "")}
536+
<Show when={svgPreviewUrl()}>
537+
<div class="flex justify-center pb-40">
538+
<img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
539+
</div>
540+
</Show>
541+
</div>
542+
</Match>
543+
<Match when={state()?.loaded && isBinary()}>
544+
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
545+
<Mark class="w-14 opacity-10" />
546+
<div class="flex flex-col gap-2 max-w-md">
547+
<div class="text-14-semibold text-text-strong truncate">{path()?.split("/").pop()}</div>
548+
<div class="text-14-regular text-text-weak">{language.t("session.files.binaryContent")}</div>
538549
</div>
539-
</Show>
540-
</div>
541-
</Match>
542-
<Match when={state()?.loaded && isBinary()}>
543-
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
544-
<Mark class="w-14 opacity-10" />
545-
<div class="flex flex-col gap-2 max-w-md">
546-
<div class="text-14-semibold text-text-strong truncate">{path()?.split("/").pop()}</div>
547-
<div class="text-14-regular text-text-weak">{language.t("session.files.binaryContent")}</div>
548550
</div>
549-
</div>
550-
</Match>
551-
<Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
552-
<Match when={state()?.loading}>
553-
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
554-
</Match>
555-
<Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
556-
</Switch>
551+
</Match>
552+
<Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
553+
<Match when={state()?.loading}>
554+
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
555+
</Match>
556+
<Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
557+
</Switch>
558+
</ScrollView>
557559
</Tabs.Content>
558560
)
559561
}

packages/app/src/pages/session/message-timeline.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
88
import { Dialog } from "@opencode-ai/ui/dialog"
99
import { InlineInput } from "@opencode-ai/ui/inline-input"
1010
import { SessionTurn } from "@opencode-ai/ui/session-turn"
11+
import { ScrollView } from "@opencode-ai/ui/scroll-view"
1112
import type { UserMessage } from "@opencode-ai/sdk/v2"
1213
import { showToast } from "@opencode-ai/ui/toast"
1314
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
@@ -322,8 +323,8 @@ export function MessageTimeline(props: {
322323
<Icon name="arrow-down-to-line" />
323324
</button>
324325
</div>
325-
<div
326-
ref={props.setScrollRef}
326+
<ScrollView
327+
viewportRef={props.setScrollRef}
327328
onWheel={(e) => {
328329
const root = e.currentTarget
329330
const delta = normalizeWheelDelta({
@@ -367,7 +368,7 @@ export function MessageTimeline(props: {
367368
if (props.isDesktop) props.onScrollSpyScroll()
368369
}}
369370
onClick={props.onAutoScrollInteraction}
370-
class="relative min-w-0 w-full h-full overflow-y-auto session-scroller"
371+
class="relative min-w-0 w-full h-full"
371372
style={{
372373
"--session-title-height": showHeader() ? "40px" : "0px",
373374
"--sticky-accordion-top": showHeader() ? "48px" : "0px",
@@ -548,7 +549,7 @@ export function MessageTimeline(props: {
548549
)}
549550
</For>
550551
</div>
551-
</div>
552+
</ScrollView>
552553
</div>
553554
</Show>
554555
)

packages/app/src/pages/session/review-tab.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
143143
open={props.view().review.open()}
144144
onOpenChange={props.view().review.setOpen}
145145
classes={{
146-
root: props.classes?.root ?? "pb-6",
146+
root: props.classes?.root ?? "pb-6 pr-3",
147147
header: props.classes?.header ?? "px-3",
148-
container: props.classes?.container ?? "px-3",
148+
container: props.classes?.container ?? "pl-3",
149149
}}
150150
diffs={props.diffs()}
151151
diffStyle={props.diffStyle}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
.scroll-view {
2+
position: relative;
3+
overflow: hidden;
4+
}
5+
6+
.scroll-view__viewport {
7+
height: 100%;
8+
width: 100%;
9+
overflow-y: auto;
10+
scrollbar-width: none;
11+
outline: none;
12+
}
13+
14+
.scroll-view__viewport::-webkit-scrollbar {
15+
display: none;
16+
}
17+
18+
.scroll-view__thumb {
19+
position: absolute;
20+
right: 0;
21+
top: 0;
22+
width: 16px;
23+
transition: opacity 200ms ease;
24+
cursor: default;
25+
user-select: none;
26+
opacity: 0;
27+
}
28+
29+
.scroll-view__thumb::after {
30+
content: "";
31+
position: absolute;
32+
right: 4px;
33+
top: 0;
34+
bottom: 0;
35+
width: 6px;
36+
border-radius: 9999px;
37+
background-color: var(--border-weak-base);
38+
backdrop-filter: blur(4px);
39+
transition: background-color 150ms ease;
40+
}
41+
42+
.scroll-view__thumb:hover::after,
43+
.scroll-view__thumb[data-dragging="true"]::after {
44+
background-color: var(--border-strong-base);
45+
}
46+
47+
.dark .scroll-view__thumb::after,
48+
[data-theme="dark"] .scroll-view__thumb::after {
49+
background-color: var(--border-weak-base);
50+
}
51+
52+
.dark .scroll-view__thumb:hover::after,
53+
[data-theme="dark"] .scroll-view__thumb:hover::after,
54+
.dark .scroll-view__thumb[data-dragging="true"]::after,
55+
[data-theme="dark"] .scroll-view__thumb[data-dragging="true"]::after {
56+
background-color: var(--border-strong-base);
57+
}
58+
59+
.scroll-view__thumb[data-visible="true"] {
60+
opacity: 1;
61+
}

0 commit comments

Comments
 (0)