Skip to content

Commit 8f3c8c8

Browse files
feat(ui): add scroll fade overlay for better scrolling (#2515)
1 parent a4854d5 commit 8f3c8c8

File tree

1 file changed

+91
-34
lines changed
  • apps/desktop/src/components/main/body/sessions/note-input

1 file changed

+91
-34
lines changed

apps/desktop/src/components/main/body/sessions/note-input/index.tsx

Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import { useCallback, useEffect, useRef, useState } from "react";
1+
import {
2+
type RefObject,
3+
useCallback,
4+
useEffect,
5+
useRef,
6+
useState,
7+
} from "react";
28
import { useHotkeys } from "react-hotkeys-hook";
9+
import { useResizeObserver } from "usehooks-ts";
310

411
import type { TiptapEditor } from "@hypr/tiptap/editor";
512
import { cn } from "@hypr/utils";
@@ -39,6 +46,10 @@ export function NoteInput({
3946
: currentTab.type,
4047
);
4148

49+
const { fadeRef, atStart, atEnd } = useScrollFade<HTMLDivElement>([
50+
currentTab,
51+
]);
52+
4253
const handleTabChange = useCallback(
4354
(view: EditorView) => {
4455
onBeforeTabChange();
@@ -78,39 +89,42 @@ export function NoteInput({
7889
/>
7990
</div>
8091

81-
<div
82-
ref={
83-
currentTab.type !== "transcript"
84-
? (node) => {
85-
scrollRef.current = node;
86-
}
87-
: undefined
88-
}
89-
onClick={handleContainerClick}
90-
className={cn([
91-
"flex-1 mt-2 px-3",
92-
currentTab.type === "transcript"
93-
? "overflow-hidden"
94-
: ["overflow-auto", "pb-6"],
95-
])}
96-
>
97-
{currentTab.type === "enhanced" && (
98-
<Enhanced
99-
ref={editorRef}
100-
sessionId={sessionId}
101-
enhancedNoteId={currentTab.id}
102-
/>
103-
)}
104-
{currentTab.type === "raw" && (
105-
<RawEditor ref={editorRef} sessionId={sessionId} />
106-
)}
107-
{currentTab.type === "transcript" && (
108-
<Transcript
109-
sessionId={sessionId}
110-
isEditing={isEditing}
111-
scrollRef={scrollRef}
112-
/>
113-
)}
92+
<div className="relative flex-1 mt-2 overflow-hidden">
93+
<div
94+
ref={(node) => {
95+
fadeRef.current = node;
96+
if (currentTab.type !== "transcript") {
97+
scrollRef.current = node;
98+
}
99+
}}
100+
onClick={handleContainerClick}
101+
className={cn([
102+
"h-full px-3",
103+
currentTab.type === "transcript"
104+
? "overflow-hidden"
105+
: ["overflow-auto", "pb-6"],
106+
])}
107+
>
108+
{currentTab.type === "enhanced" && (
109+
<Enhanced
110+
ref={editorRef}
111+
sessionId={sessionId}
112+
enhancedNoteId={currentTab.id}
113+
/>
114+
)}
115+
{currentTab.type === "raw" && (
116+
<RawEditor ref={editorRef} sessionId={sessionId} />
117+
)}
118+
{currentTab.type === "transcript" && (
119+
<Transcript
120+
sessionId={sessionId}
121+
isEditing={isEditing}
122+
scrollRef={scrollRef}
123+
/>
124+
)}
125+
</div>
126+
{!atStart && <ScrollFadeOverlay position="top" />}
127+
{!atEnd && <ScrollFadeOverlay position="bottom" />}
114128
</div>
115129
</div>
116130
);
@@ -225,3 +239,46 @@ function useTabShortcuts({
225239
[currentTab, editorTabs, handleTabChange],
226240
);
227241
}
242+
243+
function useScrollFade<T extends HTMLElement>(deps: unknown[] = []) {
244+
const fadeRef = useRef<T>(null);
245+
const [state, setState] = useState({ atStart: true, atEnd: true });
246+
247+
const update = useCallback(() => {
248+
const el = fadeRef.current;
249+
if (!el) return;
250+
251+
const { scrollTop, scrollHeight, clientHeight } = el;
252+
setState({
253+
atStart: scrollTop <= 1,
254+
atEnd: scrollTop + clientHeight >= scrollHeight - 1,
255+
});
256+
}, []);
257+
258+
useResizeObserver({ ref: fadeRef as RefObject<T>, onResize: update });
259+
260+
useEffect(() => {
261+
const el = fadeRef.current;
262+
if (!el) return;
263+
264+
update();
265+
el.addEventListener("scroll", update);
266+
return () => el.removeEventListener("scroll", update);
267+
}, [update, ...deps]);
268+
269+
return { fadeRef, ...state };
270+
}
271+
272+
function ScrollFadeOverlay({ position }: { position: "top" | "bottom" }) {
273+
return (
274+
<div
275+
className={cn([
276+
"absolute left-0 w-full h-8 z-20 pointer-events-none",
277+
position === "top" &&
278+
"top-0 bg-gradient-to-b from-white to-transparent",
279+
position === "bottom" &&
280+
"bottom-0 bg-gradient-to-t from-white to-transparent",
281+
])}
282+
/>
283+
);
284+
}

0 commit comments

Comments
 (0)