feat: add OverlayScrollbars for improved scrolling UI

Replace native ScrollArea with OverlayScrollbars library in
AbstractCard raw view and main page layout. Provides consistent,
customizable scrollbars with auto-hide behavior across the
application.
This commit is contained in:
2025-10-23 10:18:52 -05:00
parent d1b27a734a
commit a2a83e9593
6 changed files with 119 additions and 71 deletions

View File

@@ -35,6 +35,8 @@
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"next": "^15.5.6", "next": "^15.5.6",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"overlayscrollbars": "^2.12.0",
"overlayscrollbars-react": "^0.5.6",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-hook-form": "^7.42.1", "react-hook-form": "^7.42.1",

22
pnpm-lock.yaml generated
View File

@@ -44,6 +44,12 @@ importers:
next-themes: next-themes:
specifier: ^0.4.6 specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
overlayscrollbars:
specifier: ^2.12.0
version: 2.12.0
overlayscrollbars-react:
specifier: ^0.5.6
version: 0.5.6(overlayscrollbars@2.12.0)(react@19.2.0)
react: react:
specifier: 19.2.0 specifier: 19.2.0
version: 19.2.0 version: 19.2.0
@@ -3328,6 +3334,15 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
overlayscrollbars-react@0.5.6:
resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==}
peerDependencies:
overlayscrollbars: ^2.0.0
react: '>=16.8.0'
overlayscrollbars@2.12.0:
resolution: {integrity: sha512-mWJ5MOkcZ/ljHwfLw8+bN0V9ziGCoNoqULcp994j5DTGNQvnkWKWkA7rnO29Kyew5AoHxUnJ4Ndqfcl0HSQjXg==}
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'}
@@ -7363,6 +7378,13 @@ snapshots:
type-check: 0.4.0 type-check: 0.4.0
word-wrap: 1.2.5 word-wrap: 1.2.5
overlayscrollbars-react@0.5.6(overlayscrollbars@2.12.0)(react@19.2.0):
dependencies:
overlayscrollbars: 2.12.0
react: 19.2.0
overlayscrollbars@2.12.0: {}
own-keys@1.0.1: own-keys@1.0.1:
dependencies: dependencies:
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0

View File

@@ -2,7 +2,8 @@ import type { FunctionComponent, ReactNode } from "react";
import React from "react"; import React from "react";
import { useBoolean } from "usehooks-ts"; import { useBoolean } from "usehooks-ts";
import { Link2Icon, CodeIcon, DownloadIcon, ClipboardIcon } from "@radix-ui/react-icons"; import { Link2Icon, CodeIcon, DownloadIcon, ClipboardIcon } from "@radix-ui/react-icons";
import { Card, Flex, Box, IconButton, Code, ScrollArea, Tooltip } from "@radix-ui/themes"; import { Card, Flex, Box, IconButton, Code, Tooltip } from "@radix-ui/themes";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
type AbstractCardProps = { type AbstractCardProps = {
children?: ReactNode; children?: ReactNode;
@@ -125,7 +126,16 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
)} )}
<Box p="4"> <Box p="4">
{showRaw ? ( {showRaw ? (
<ScrollArea type="auto" scrollbars="both" style={{ maxHeight: "40rem" }}> <OverlayScrollbarsComponent
defer
options={{
scrollbars: {
autoHide: "leave",
autoHideDelay: 1300,
},
}}
style={{ maxHeight: "40rem" }}
>
<Code <Code
variant="ghost" variant="ghost"
size="2" size="2"
@@ -137,7 +147,7 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
> >
{JSON.stringify(data, null, 4)} {JSON.stringify(data, null, 4)}
</Code> </Code>
</ScrollArea> </OverlayScrollbarsComponent>
) : ( ) : (
children children
)} )}

View File

@@ -5,6 +5,7 @@ import { type AppType } from "next/dist/shared/lib/utils";
import "@fontsource-variable/inter"; import "@fontsource-variable/inter";
import "@fontsource/ibm-plex-mono/400.css"; import "@fontsource/ibm-plex-mono/400.css";
import "@radix-ui/themes/styles.css"; import "@radix-ui/themes/styles.css";
import "overlayscrollbars/overlayscrollbars.css";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { DateFormatProvider } from "@/contexts/DateFormatContext"; import { DateFormatProvider } from "@/contexts/DateFormatContext";

View File

@@ -10,6 +10,7 @@ import { ThemeToggle } from "@/components/ThemeToggle";
import { Maybe } from "true-myth"; import { Maybe } from "true-myth";
import { Flex, Container, Section, Text, Link, IconButton } from "@radix-ui/themes"; import { Flex, Container, Section, Text, Link, IconButton } from "@radix-ui/themes";
import { GitHubLogoIcon } from "@radix-ui/react-icons"; import { GitHubLogoIcon } from "@radix-ui/react-icons";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
const Index: NextPage = () => { const Index: NextPage = () => {
const { error, setTarget, setTargetType, submit, currentType } = useLookup(); const { error, setTarget, setTargetType, submit, currentType } = useLookup();
@@ -38,78 +39,89 @@ const Index: NextPage = () => {
content="xevion, rdap, whois, rdap, domain name, dns, ip address" content="xevion, rdap, whois, rdap, domain name, dns, ip address"
/> />
</Head> </Head>
<Flex <OverlayScrollbarsComponent
asChild defer
justify="between" options={{
align="center" scrollbars: {
px="5" autoHide: "leave",
py="4" autoHideDelay: 1300,
style={{ },
borderBottom: "1px solid var(--gray-a5)",
}} }}
style={{ height: "100vh" }}
> >
<nav> <Flex
<Text size="5" weight="medium"> asChild
<Link href="https://github.com/Xevion/rdap" color="gray" highContrast> justify="between"
rdap align="center"
</Link> px="5"
<Link href="https://xevion.dev" color="gray"> py="4"
.xevion.dev style={{
</Link> borderBottom: "1px solid var(--gray-a5)",
</Text> }}
<Flex gap="4" align="center"> >
<IconButton <nav>
asChild <Text size="5" weight="medium">
size="3" <Link href="https://github.com/Xevion/rdap" color="gray" highContrast>
variant="ghost" rdap
aria-label="View on GitHub" </Link>
title="View on GitHub" <Link href="https://xevion.dev" color="gray">
> .xevion.dev
<a </Link>
href="https://github.com/Xevion/rdap" </Text>
target="_blank" <Flex gap="4" align="center">
rel="noopener noreferrer" <IconButton
asChild
size="3"
variant="ghost"
aria-label="View on GitHub"
title="View on GitHub"
> >
<GitHubLogoIcon width="22" height="22" /> <a
</a> href="https://github.com/Xevion/rdap"
</IconButton> target="_blank"
<ThemeToggle /> rel="noopener noreferrer"
</Flex> >
</nav> <GitHubLogoIcon width="22" height="22" />
</Flex> </a>
<Container size="3" px="5"> </IconButton>
<Section size="2"> <ThemeToggle />
<LookupInput </Flex>
isLoading={isLoading} </nav>
detectedType={currentType} </Flex>
onChange={({ target, targetType }) => { <Container size="3" px="5">
setTarget(target); <Section size="2">
setTargetType(targetType); <LookupInput
}} isLoading={isLoading}
onSubmit={async function (props) { detectedType={currentType}
try { onChange={({ target, targetType }) => {
setLoading(true); setTarget(target);
setResponse(await submit(props)); setTargetType(targetType);
setLoading(false); }}
} catch (e) { onSubmit={async function (props) {
console.error(e); try {
setResponse(Maybe.nothing()); setLoading(true);
setLoading(false); setResponse(await submit(props));
} setLoading(false);
}} } catch (e) {
/> console.error(e);
{error != null ? ( setResponse(Maybe.nothing());
<ErrorCard setLoading(false);
title="An error occurred while performing a lookup." }
description={error} }}
className="mb-2"
/> />
) : null} {error != null ? (
{response.isJust ? ( <ErrorCard
<Generic url={response.value.url} data={response.value.data} /> title="An error occurred while performing a lookup."
) : null} description={error}
</Section> className="mb-2"
</Container> />
) : null}
{response.isJust ? (
<Generic url={response.value.url} data={response.value.data} />
) : null}
</Section>
</Container>
</OverlayScrollbarsComponent>
</> </>
); );
}; };

View File

@@ -157,6 +157,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
size="3" size="3"
placeholder={placeholders[selected]} placeholder={placeholders[selected]}
disabled={isLoading} disabled={isLoading}
autoFocus
onFocus={() => { onFocus={() => {
isFocusedRef.current = true; isFocusedRef.current = true;
}} }}