pref: overlay scrollbar

This commit is contained in:
Ou 2024-10-10 21:32:13 +08:00
parent a89edff8fa
commit 077b251ff7
11 changed files with 113 additions and 138 deletions

View File

@ -30,6 +30,7 @@
"consola": "^3.2.3",
"dayjs": "1.11.13",
"db0": "npm:@ourongxing/db0@latest",
"defu": "^6.1.4",
"fast-xml-parser": "^4.5.0",
"favicons-scraper": "^1.3.2",
"framer-motion": "^11.11.5",

3
pnpm-lock.yaml generated
View File

@ -56,6 +56,9 @@ importers:
db0:
specifier: npm:@ourongxing/db0@latest
version: '@ourongxing/db0@0.1.5(libsql@0.4.5)'
defu:
specifier: ^6.1.4
version: 6.1.4
fast-xml-parser:
specifier: ^4.5.0
version: 4.5.0

View File

@ -13,11 +13,11 @@ export const metadata: Metadata = {
},
china: {
name: "国内",
sources: ["toutiao", "zhihu", "cankaoxiaoxi"],
sources: ["toutiao", "zhihu"],
},
world: {
name: "国际",
sources: ["sputniknewscn", "zaobao"],
sources: ["sputniknewscn", "zaobao", "cankaoxiaoxi"],
},
code: {
name: "代码",

View File

@ -0,0 +1,43 @@
import type { UseOverlayScrollbarsParams } from "overlayscrollbars-react"
import { useOverlayScrollbars } from "overlayscrollbars-react"
import type { HTMLProps, PropsWithChildren } from "react"
import { useEffect, useMemo, useRef } from "react"
import { defu } from "defu"
type Props = HTMLProps<HTMLDivElement> & UseOverlayScrollbarsParams
const defaultScrollbarParams: UseOverlayScrollbarsParams = {
options: {
scrollbars: {
autoHide: "scroll",
},
},
defer: true,
}
export function OverlayScrollbar({ children, options, events, defer, ...props }: PropsWithChildren<Props>) {
const ref = useRef<HTMLDivElement>(null)
const scrollbarParams = useMemo(() => defu<UseOverlayScrollbarsParams, Array<UseOverlayScrollbarsParams> >({
options,
events,
defer,
}, defaultScrollbarParams), [options, events, defer])
const [initialize] = useOverlayScrollbars(scrollbarParams)
useEffect(() => {
initialize({
target: ref.current!,
cancel: {
// 如果浏览器原生滚动条是覆盖在元素上的,则取消初始化
nativeScrollbarsOverlaid: true,
},
})
}, [initialize])
return (
<div ref={ref} {...props}>
{/* 只能有一个 element */}
<div>{children}</div>
</div>
)
}

View File

@ -44,7 +44,7 @@ function RefreshButton() {
export function Header() {
return (
<header className="flex justify-between items-center">
<header className="flex justify-between items-center bg-base p-4 md:(p-8)">
<Link className="text-6 flex gap-2 items-center" to="/">
<img src={logo} alt="logo" className="h-8" />
<span className="font-mono">NewsNow</span>

View File

@ -1,5 +1,4 @@
import type { NewsItem, SourceID, SourceInfo, SourceResponse } from "@shared/types"
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
import type { UseQueryResult } from "@tanstack/react-query"
import { useQuery } from "@tanstack/react-query"
import clsx from "clsx"
@ -9,6 +8,7 @@ import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"
import { sources } from "@shared/sources"
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
import { ofetch } from "ofetch"
import { OverlayScrollbar } from "../common/overlay-scrollbar"
import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms"
import { useRelativeTime } from "~/hooks/useRelativeTime"
@ -46,7 +46,8 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
<div
ref={ref}
className={clsx(
"flex flex-col h-500px aspect-auto border border-gray-100 rounded-xl shadow-2xl shadow-gray-600/10 bg-base dark:( border-gray-700 shadow-none)",
"flex flex-col h-500px aspect-auto border border-gray-100 rounded-xl shadow-2xl shadow-gray-600/10 bg-base",
"dark:( border-gray-700 shadow-none)",
isDragged && "op-50",
isOverlay ? "bg-glass" : "",
)}
@ -98,12 +99,7 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
return (
<>
<div
{...handleListeners}
className={clsx([
"flex justify-between p-2 items-center",
handleListeners && "cursor-grab",
isOverlay && "cursor-grabbing",
])}
className={clsx("flex justify-between p-2 items-center")}
>
<div className="flex items-center gap-2">
<img src={`/icons/${id.split("-")[0]}.png`} className="w-4 h-4 rounded" alt={id} onError={e => e.currentTarget.hidden = true} />
@ -111,16 +107,16 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
{sources[id].name}
</span>
</div>
<span className="text-xs">{sources[id]?.title}</span>
<div className="flex gap-2 items-center">
<span className="text-xs">{sources[id]?.title}</span>
<button
{...handleListeners}
type="button"
className={clsx("i-ph:dots-six-vertical-bold op-40 hover:op-80", handleListeners && "cursor-grab", isOverlay && "cursor-grabbing")}
/>
</div>
</div>
<OverlayScrollbarsComponent
defer
className="h-full pl-2 pr-3 mr-1"
element="div"
options={{ scrollbars: { autoHide: "scroll" }, overflow: { x: "hidden" } }}
>
<NewsList query={query} />
</OverlayScrollbarsComponent>
<NewsList query={query} />
<div className="p-2 flex items-center justify-between">
<UpdateTime query={query} />
<div className="flex gap-1">
@ -140,7 +136,6 @@ function UpdateTime({ query }: Query) {
const updatedTime = useRelativeTime(query.data?.updatedTime ?? "")
if (updatedTime) return <span>{`${updatedTime}更新`}</span>
if (query.isError) return <span></span>
return <span className="skeleton w-20" />
}
function Num({ num }: { num: number }) {
@ -159,7 +154,7 @@ function ExtraInfo({ item }: { item: NewsItem }) {
}
if (item?.extra?.icon) {
return <img src={item.extra.icon} className="w-5 inline" />
return <img src={item.extra.icon} className="w-5 inline" onError={e => e.currentTarget.hidden = true} />
}
if (relativeTime) {
@ -169,33 +164,26 @@ function ExtraInfo({ item }: { item: NewsItem }) {
function NewsList({ query }: Query) {
const items = query.data?.items
if (items?.length) {
return (
<>
{items.slice(0, 20).map((item, i) => (
<div key={item.title} className="flex gap-2 items-center">
<Num num={i + 1} />
<a href={item.url} target="_blank" className="my-1">
<span className="mr-2">
{item.title}
</span>
<span className="text-xs text-gray-4/80 truncate align-middle">
<ExtraInfo item={item} />
</span>
</a>
</div>
))}
</>
)
}
return (
<>
{Array.from({ length: 20 }).map((_, i) => i).map(i => (
<div key={i} className="flex gap-2 items-center">
<OverlayScrollbar
className="h-full pl-2 pr-3 mr-1 overflow-x-auto"
options={{
overflow: { x: "hidden" },
}}
>
{items?.slice(0, 20).map((item, i) => (
<div key={item.title} className="flex gap-2 items-center">
<Num num={i + 1} />
<span className="skeleton border-b border-gray-300/20 my-1"></span>
<a href={item.url} target="_blank" className="my-1">
<span className="mr-2">
{item.title}
</span>
<span className="text-xs text-gray-4/80 truncate align-middle">
<ExtraInfo item={item} />
</span>
</a>
</div>
))}
</>
</OverlayScrollbar>
)
}

View File

@ -24,9 +24,9 @@ export function Dnd() {
return (
<DndWrapper items={items} setItems={setItems}>
<motion.div
className="grid w-full gap-5"
className="grid w-full gap-4"
style={{
gridTemplateColumns: "repeat(auto-fill, minmax(350px, 1fr))",
gridTemplateColumns: "repeat(auto-fill, minmax(325px, 1fr))",
}}
initial="hidden"
animate="visible"
@ -34,7 +34,7 @@ export function Dnd() {
visible: {
transition: {
delayChildren: 0.5,
staggerChildren: 0.3,
staggerChildren: 0.2,
},
},
}}

View File

@ -14,23 +14,25 @@ export function Section({ id }: { id: SectionID }) {
}, [id, setCurrentSectionID])
return (
<div className="flex flex-col justify-center items-center">
<section className="flex gap-2 py-4 sm:mt--12">
{sectionIds.map(section => (
<Link
key={section}
to="/s/$section"
params={{ section }}
className={clsx(
"btn-action-sm",
id === section && "btn-action-active",
)}
>
{metadata[section].name}
</Link>
))}
</section>
<>
<div className="w-full flex justify-center">
<div className="flex gap-2 py-4">
{sectionIds.map(section => (
<Link
key={section}
to="/s/$section"
params={{ section }}
className={clsx(
"btn-action-sm",
id === section && "btn-action-active",
)}
>
{metadata[section].name}
</Link>
))}
</div>
</div>
{ currentSectionID === id && <Dnd />}
</div>
</>
)
}

View File

@ -4,9 +4,9 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import "~/styles/globals.css"
import "virtual:uno.css"
import type { QueryClient } from "@tanstack/react-query"
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
import { Header } from "~/components/header"
import { useOnReload } from "~/hooks/useOnReload"
import { OverlayScrollbar } from "~/components/common/overlay-scrollbar"
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
@ -25,23 +25,17 @@ function NotFoundComponent() {
function RootComponent() {
useOnReload()
return (
<OverlayScrollbarsComponent
defer
className="md:p-10 p-4 h-full"
element="div"
options={{
showNativeOverlaidScrollbars: true,
scrollbars: { autoHide: "scroll" },
}}
>
<Header />
<Outlet />
<>
<OverlayScrollbar className="md:p-8 p-4 h-full overflow-x-auto">
<Header />
<Outlet />
</OverlayScrollbar>
{ import.meta.env.DEV && (
<>
<ReactQueryDevtools buttonPosition="bottom-left" />
<TanStackRouterDevtools position="bottom-right" />
</>
)}
</OverlayScrollbarsComponent>
</>
)
}

View File

@ -27,8 +27,8 @@ button:disabled {
pointer-events: all !important;
}
::-webkit-scrollbar {
width: 0px;
::-webkit-scrollbar-thumb {
border-radius: 8px;
}
/* https://github.com/KingSora/OverlayScrollbars/blob/master/packages/overlayscrollbars/src/styles/themes.scss */

View File

@ -31,36 +31,8 @@ export default defineConfig({
},
theme: {
colors: {
neutral: {
50: "#FCFCFD",
100: "#F9FAFB",
200: "#F2F4F7",
300: "#E4E7EC",
400: "#D0D5DD",
500: "#98A2B3",
600: "#667085",
700: "#475467",
800: "#344054",
900: "#1D2939",
950: "#101828",
},
primary: {
DEFAULT: "#34B49B",
50: "#EFFAF8",
100: "#DBF5F0",
200: "#B8EAE0",
300: "#88DDCC",
400: "#51CDB4",
500: "#34B49B",
600: "#2FA28B",
700: "#298E7A",
800: "#227766",
900: "#185347",
950: "#123F36",
},
warning: {
DEFAULT: "#FDB022",
50: "#FFFCF5",
100: "#FFFAEB",
200: "#FEF0C7",
@ -73,34 +45,6 @@ export default defineConfig({
900: "#93370D",
950: "#7A2E0E",
},
success: {
50: "#F6FEF9",
100: "#ECFDF3",
200: "#D1FADF",
300: "#A6F4C5",
400: "#6CE9A6",
500: "#32D583",
600: "#12B76A",
700: "#039855",
800: "#027A48",
900: "#05603A",
950: "#054F31",
},
rose: {
50: "#FFF5F6",
100: "#FFF1F3",
200: "#FFE4E8",
300: "#FECDD6",
400: "#FEA3B4",
500: "#FD6F8E",
600: "#F63D68",
700: "#E31B54",
800: "#C01048",
900: "#A11043",
950: "#89123E",
},
},
},
})