mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
pref: overlay scrollbar
This commit is contained in:
parent
a89edff8fa
commit
077b251ff7
@ -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
3
pnpm-lock.yaml
generated
@ -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
|
||||
|
@ -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: "代码",
|
||||
|
43
src/components/common/overlay-scrollbar.tsx
Normal file
43
src/components/common/overlay-scrollbar.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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 */
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user