feat: add AutoResizeComponent and entry content header for metadata (#72)
* feat: add component Signed-off-by: Innei <i@innei.in> * feat: entry title meta Signed-off-by: Innei <i@innei.in> * fix: update anchor Signed-off-by: Innei <i@innei.in> * fix: ts error Signed-off-by: Innei <i@innei.in> --------- Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
302dc4c7cd
commit
7372034c69
|
|
@ -58,6 +58,7 @@
|
|||
"dotenv": "16.4.5",
|
||||
"electron-store": "8.2.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"foxact": "0.2.35",
|
||||
"framer-motion": "11.2.9",
|
||||
"franc-min": "6.2.0",
|
||||
"fuse.js": "7.0.0",
|
||||
|
|
|
|||
|
|
@ -118,6 +118,9 @@ importers:
|
|||
electron-updater:
|
||||
specifier: ^6.1.7
|
||||
version: 6.1.8
|
||||
foxact:
|
||||
specifier: 0.2.35
|
||||
version: 0.2.35(react@18.3.1)
|
||||
framer-motion:
|
||||
specifier: 11.2.9
|
||||
version: 11.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
|
@ -2404,6 +2407,9 @@ packages:
|
|||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -3179,6 +3185,14 @@ packages:
|
|||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
foxact@0.2.35:
|
||||
resolution: {integrity: sha512-5e2p34TcBETZTTMhmf4WuljS07lgtv3laimqJer9OGOqYgOpYHgKfR0Cie8Q2IxH1FFhVkm1l0+2W/XL8nBL7Q==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
framer-motion@11.2.9:
|
||||
resolution: {integrity: sha512-gfxNSkp4dC3vpy2hGNQK3K9bNOKwfasqOhrqvmZzYxCPSJ9Tpv/9JlCkeCMgFdKefgPr8+JiouGjVmaDzu750w==}
|
||||
peerDependencies:
|
||||
|
|
@ -4798,6 +4812,9 @@ packages:
|
|||
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
server-only@0.0.1:
|
||||
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
|
||||
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
|
|
@ -7881,6 +7898,8 @@ snapshots:
|
|||
- '@types/react'
|
||||
- react-dom
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
|
@ -8931,6 +8950,13 @@ snapshots:
|
|||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
foxact@0.2.35(react@18.3.1):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
server-only: 0.0.1
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
framer-motion@11.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
|
|
@ -10750,6 +10776,8 @@ snapshots:
|
|||
type-fest: 0.13.1
|
||||
optional: true
|
||||
|
||||
server-only@0.0.1: {}
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
"use client"
|
||||
|
||||
import type { JSX } from "react"
|
||||
import { cloneElement } from "react"
|
||||
|
||||
export const ProviderComposer: Component<{
|
||||
contexts: JSX.Element[]
|
||||
}> = ({ contexts, children }) =>
|
||||
contexts.reduceRight(
|
||||
(kids: any, parent: any) => cloneElement(parent, { children: kids }),
|
||||
children,
|
||||
)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { cn } from "@renderer/lib/utils"
|
||||
import type { Spring } from "framer-motion"
|
||||
import { m } from "framer-motion"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
const softSpringPreset: Spring = {
|
||||
duration: 0.35,
|
||||
type: "spring",
|
||||
stiffness: 120,
|
||||
damping: 20,
|
||||
}
|
||||
|
||||
interface AnimateChangeInHeightProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
duration?: number
|
||||
|
||||
spring?: boolean
|
||||
}
|
||||
|
||||
export const AutoResizeHeight: React.FC<AnimateChangeInHeightProps> = ({
|
||||
children,
|
||||
className,
|
||||
duration = 0.6,
|
||||
spring = false,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const [height, setHeight] = useState<number | "auto">("auto")
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
// We only have one entry, so we can use entries[0].
|
||||
const observedHeight = entries[0].contentRect.height
|
||||
setHeight(observedHeight)
|
||||
})
|
||||
|
||||
resizeObserver.observe(containerRef.current)
|
||||
|
||||
return () => {
|
||||
// Cleanup the observer when the component is unmounted
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<m.div
|
||||
className={cn("overflow-hidden", className)}
|
||||
style={{ height }}
|
||||
initial={false}
|
||||
animate={{ height }}
|
||||
transition={spring ? softSpringPreset : { duration }}
|
||||
>
|
||||
<div ref={containerRef}>{children}</div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import type { PrimitiveAtom } from "jotai"
|
||||
import type { Atom, PrimitiveAtom } from "jotai"
|
||||
import { createStore, useAtom, useAtomValue, useSetAtom } from "jotai"
|
||||
import { selectAtom } from "jotai/utils"
|
||||
import { useCallback } from "react"
|
||||
|
||||
export const jotaiStore = createStore()
|
||||
|
||||
|
|
@ -21,3 +23,16 @@ export const createAtomHooks = <T>(atom: PrimitiveAtom<T>) =>
|
|||
() => useSetAtom(atom),
|
||||
...createAtomAccessor(atom),
|
||||
] as const
|
||||
|
||||
export const createAtomSelector = <T>(atom: Atom<T>) => {
|
||||
const useHook = <R>(selector: (a: T) => R, deps: any[] = []) =>
|
||||
useAtomValue(
|
||||
selectAtom(
|
||||
atom,
|
||||
useCallback((a) => selector(a as T), deps),
|
||||
),
|
||||
)
|
||||
|
||||
useHook.__atom = atom
|
||||
return useHook
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
/* eslint-disable unicorn/no-unreadable-array-destructuring */
|
||||
|
||||
import { createAtomHooks } from "@renderer/lib/jotai"
|
||||
import { atom } from "jotai"
|
||||
|
||||
export const [, , useEntryTitleMeta, , getEntryTitleMeta, setEntryTitleMeta] =
|
||||
createAtomHooks(
|
||||
atom(
|
||||
null as Nullable<{
|
||||
title: string
|
||||
description: string
|
||||
}>,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { ActionButton } from "@renderer/components/ui/button"
|
||||
import { useEntryActions } from "@renderer/hooks"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { useEntry } from "@renderer/store/entry/hooks"
|
||||
import { AnimatePresence, m } from "framer-motion"
|
||||
|
||||
import { useEntryTitleMeta } from "./atoms"
|
||||
|
||||
export function EntryHeader({
|
||||
view,
|
||||
entryId,
|
||||
}: {
|
||||
view: number
|
||||
entryId: string
|
||||
}) {
|
||||
const entry = useEntry(entryId)
|
||||
|
||||
const { items } = useEntryActions({
|
||||
view,
|
||||
entry,
|
||||
})
|
||||
const entryTitleMeta = useEntryTitleMeta()
|
||||
if (!entry?.entries.url) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-[55px] min-w-0 items-center justify-between gap-3 overflow-hidden px-5 text-lg text-zinc-500",
|
||||
entryTitleMeta && "border-b border-border",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 shrink">
|
||||
<AnimatePresence>
|
||||
{entryTitleMeta && (
|
||||
<m.div
|
||||
initial={{ opacity: 0.01, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0.01, y: 30 }}
|
||||
className="flex min-w-0 shrink items-end gap-2 truncate text-sm leading-tight text-theme-foreground"
|
||||
>
|
||||
<span className="min-w-0 shrink truncate font-bold">{entryTitleMeta.title}</span>
|
||||
<i className="i-mingcute-line-line size-[10px] shrink-0 translate-y-[-3px] rotate-[-25deg]" />
|
||||
<span className="shrink-0 truncate text-xs opacity-80">
|
||||
{entryTitleMeta.description}
|
||||
</span>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{items
|
||||
.filter((item) => !item.disabled)
|
||||
.map((item) => (
|
||||
<ActionButton
|
||||
icon={
|
||||
item.icon ? (
|
||||
<img className="size-4 grayscale" src={item.icon} />
|
||||
) : (
|
||||
<i className={item.className} />
|
||||
)
|
||||
}
|
||||
onClick={item.onClick}
|
||||
tooltip={item.name}
|
||||
key={item.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
import { Logo } from "@renderer/components/icons/logo"
|
||||
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
|
||||
import { useBizQuery } from "@renderer/hooks"
|
||||
import { parseHtml } from "@renderer/lib/parse-html"
|
||||
import type { ActiveEntryId } from "@renderer/models"
|
||||
import {
|
||||
useIsSoFWrappedElement,
|
||||
WrappedElementProvider,
|
||||
} from "@renderer/providers/wrapped-element-provider"
|
||||
import { Queries } from "@renderer/queries"
|
||||
import { useEntry, useFeedStore } from "@renderer/store"
|
||||
import { m } from "framer-motion"
|
||||
|
|
@ -9,7 +14,8 @@ import { useEffect, useState } from "react"
|
|||
|
||||
import { LoadingCircle } from "../../components/ui/loading"
|
||||
import { EntryTranslation } from "../entry-column/translation"
|
||||
import { EntryShare } from "./share"
|
||||
import { setEntryTitleMeta } from "./atoms"
|
||||
import { EntryHeader } from "./header"
|
||||
|
||||
export const EntryContent = ({ entry }: { entry: ActiveEntryId }) => {
|
||||
const activeList = useFeedStore((state) => state.activeList)
|
||||
|
|
@ -17,7 +23,7 @@ export const EntryContent = ({ entry }: { entry: ActiveEntryId }) => {
|
|||
if (!entry) {
|
||||
return (
|
||||
<m.div
|
||||
className="-mt-2 flex size-full flex-col items-center justify-center gap-1 text-lg font-medium text-zinc-400"
|
||||
className="-mt-2 flex size-full min-w-0 flex-col items-center justify-center gap-1 text-lg font-medium text-zinc-400"
|
||||
initial={{ opacity: 0.01, y: 300 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
|
|
@ -74,8 +80,8 @@ function EntryContentRender({ entryId }: { entryId: string }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<EntryShare entryId={entry.entries.id} view={0} />
|
||||
<div className="h-[calc(100%-3.5rem)] overflow-y-auto @container">
|
||||
<EntryHeader entryId={entry.entries.id} view={0} />
|
||||
<div className="h-[calc(100%-3.5rem)] min-w-0 overflow-y-auto @container">
|
||||
<m.div
|
||||
className="p-5"
|
||||
initial={{ opacity: 0.01, y: 100 }}
|
||||
|
|
@ -101,23 +107,31 @@ function EntryContentRender({ entryId }: { entryId: string }) {
|
|||
</div>
|
||||
<div className="text-[13px] text-zinc-500">
|
||||
{entry.entries.publishedAt &&
|
||||
new Date(entry.entries.publishedAt).toUTCString()}
|
||||
new Date(entry.entries.publishedAt).toLocaleString()}
|
||||
</div>
|
||||
</a>
|
||||
<div className="prose prose-zinc mx-auto mb-32 mt-8 max-w-full cursor-auto select-text break-all text-[15px] dark:prose-invert">
|
||||
{(summary.isLoading || summary.data) && (
|
||||
<div className="my-8 space-y-1 rounded-lg border px-4 py-3">
|
||||
<div className="flex items-center gap-2 font-medium text-zinc-800">
|
||||
<i className="i-mingcute-bling-line align-middle" />
|
||||
<span>AI summary</span>
|
||||
<WrappedElementProvider boundingDetection>
|
||||
<TitleMetaHandler entryId={entry.entries.id} />
|
||||
<div className="prose prose-zinc mx-auto mb-32 mt-8 max-w-full cursor-auto select-text break-all text-[15px] dark:prose-invert">
|
||||
{(summary.isLoading || summary.data) && (
|
||||
<div className="my-8 space-y-1 rounded-lg border px-4 py-3">
|
||||
<div className="flex items-center gap-2 font-medium text-zinc-800">
|
||||
<i className="i-mingcute-bling-line align-middle" />
|
||||
<span>AI summary</span>
|
||||
</div>
|
||||
<AutoResizeHeight
|
||||
spring
|
||||
className="text-sm leading-relaxed"
|
||||
>
|
||||
{summary.isLoading ?
|
||||
SummaryLoadingSkeleton :
|
||||
summary.data}
|
||||
</AutoResizeHeight>
|
||||
</div>
|
||||
<div className="text-sm leading-relaxed">
|
||||
{summary.isLoading ? "Loading..." : summary.data}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
</WrappedElementProvider>
|
||||
{!content && (
|
||||
<div className="center mt-16">
|
||||
{!error ? (
|
||||
|
|
@ -136,3 +150,29 @@ function EntryContentRender({ entryId }: { entryId: string }) {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SummaryLoadingSkeleton = (
|
||||
<div className="space-y-2">
|
||||
<span className="block h-3 w-full animate-pulse rounded-xl bg-zinc-200 dark:bg-neutral-800" />
|
||||
<span className="block h-3 w-full animate-pulse rounded-xl bg-zinc-200 dark:bg-neutral-800" />
|
||||
<span className="block h-3 w-full animate-pulse rounded-xl bg-zinc-200 dark:bg-neutral-800" />
|
||||
</div>
|
||||
)
|
||||
|
||||
const TitleMetaHandler: Component<{
|
||||
entryId: string
|
||||
}> = ({ entryId }) => {
|
||||
const isAtTop = useIsSoFWrappedElement()
|
||||
const {
|
||||
entries: { title: entryTitle },
|
||||
feeds: { title: feedTitle },
|
||||
} = useEntry(entryId)!
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAtTop && entryTitle && feedTitle) { setEntryTitleMeta({ title: entryTitle, description: feedTitle }) }
|
||||
return () => {
|
||||
setEntryTitleMeta(null)
|
||||
}
|
||||
}, [entryId, entryTitle, feedTitle, isAtTop])
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import { ActionButton } from "@renderer/components/ui/button"
|
||||
import {
|
||||
TooltipProvider,
|
||||
} from "@renderer/components/ui/tooltip"
|
||||
import { useEntryActions } from "@renderer/hooks"
|
||||
import { useEntry } from "@renderer/store/entry/hooks"
|
||||
|
||||
export function EntryShare({
|
||||
view,
|
||||
entryId,
|
||||
}: {
|
||||
view: number
|
||||
entryId: string
|
||||
}) {
|
||||
const entry = useEntry(entryId)
|
||||
|
||||
const { items } = useEntryActions({
|
||||
view,
|
||||
entry,
|
||||
})
|
||||
if (!entry?.entries.url) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-3 px-5 pt-2.5 text-lg text-zinc-500">
|
||||
<TooltipProvider>
|
||||
{items
|
||||
.filter((item) => !item.disabled)
|
||||
.map((item) => (
|
||||
<ActionButton
|
||||
icon={
|
||||
item.icon ?
|
||||
(
|
||||
<img className="size-4 grayscale" src={item.icon} />
|
||||
) :
|
||||
(
|
||||
<i className={item.className} />
|
||||
)
|
||||
}
|
||||
onClick={item.onClick}
|
||||
tooltip={item.name}
|
||||
key={item.name}
|
||||
/>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -43,8 +43,7 @@ export function Component() {
|
|||
|
||||
const inWideMode = activeList && views[activeList.view].wideMode
|
||||
return (
|
||||
<div ref={containerRef} className="flex grow">
|
||||
|
||||
<div ref={containerRef} className="flex min-w-0 grow">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full shrink-0 overflow-y-auto",
|
||||
|
|
@ -65,7 +64,7 @@ export function Component() {
|
|||
)}
|
||||
<AnimatePresence>
|
||||
{!inWideMode && (
|
||||
<div className="flex-1">
|
||||
<div className="min-w-0 flex-1">
|
||||
<EntryContent entry={activeEntry} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ export function Component() {
|
|||
<div className="w-64 shrink-0 border-r">
|
||||
<FeedColumn />
|
||||
</div>
|
||||
<main ref={setMainContainerElement} className="flex flex-1 bg-theme-background !outline-none" tabIndex={-1}>
|
||||
{/* NOTE: tabIndex for main element can get by `document.activeElement` */}
|
||||
<main ref={setMainContainerElement} className="flex min-w-0 flex-1 bg-theme-background !outline-none" tabIndex={-1}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
import { ProviderComposer } from "@renderer/components/common/ProviderComposer"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { createContextState } from "foxact/create-context-state"
|
||||
import { useIsomorphicLayoutEffect } from "foxact/use-isomorphic-layout-effect"
|
||||
import { memo, useEffect, useRef } from "react"
|
||||
|
||||
const [
|
||||
WrappedElementProviderInternal,
|
||||
useWrappedElement,
|
||||
useSetWrappedElement,
|
||||
] = createContextState<HTMLDivElement | null>(undefined as any)
|
||||
|
||||
const [
|
||||
ElementSizeProviderInternal,
|
||||
useWrappedElementSize,
|
||||
useSetWrappedElementSize,
|
||||
] = createContextState({
|
||||
h: 0,
|
||||
w: 0,
|
||||
})
|
||||
|
||||
const [
|
||||
ElementPositionProviderInternal,
|
||||
useWrappedElementPosition,
|
||||
useSetElementPosition,
|
||||
] = createContextState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
|
||||
const [
|
||||
IsEndOfElementProviderInternal,
|
||||
useIsEoFWrappedElement,
|
||||
useSetIsEOfElement,
|
||||
] = createContextState<boolean>(false)
|
||||
|
||||
const [
|
||||
IsStartOfElementProviderInternal,
|
||||
useIsSoFWrappedElement,
|
||||
useSetIsSOfElement,
|
||||
] = createContextState<boolean>(false)
|
||||
|
||||
const Providers = [
|
||||
<WrappedElementProviderInternal key="ArticleElementProviderInternal" />,
|
||||
<ElementSizeProviderInternal key="ElementSizeProviderInternal" />,
|
||||
<ElementPositionProviderInternal key="ElementPositionProviderInternal" />,
|
||||
<IsEndOfElementProviderInternal key="IsEndOfElementProviderInternal" />,
|
||||
<IsStartOfElementProviderInternal key="IsStartOfElementProviderInternal" />,
|
||||
]
|
||||
|
||||
interface WrappedElementProviderProps {
|
||||
boundingDetection?: boolean
|
||||
as?: keyof React.JSX.IntrinsicElements
|
||||
}
|
||||
|
||||
export const WrappedElementProvider: Component<WrappedElementProviderProps> = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<ProviderComposer contexts={Providers}>
|
||||
<ArticleElementResizeObserver />
|
||||
<Content {...props} className={className}>
|
||||
{children}
|
||||
</Content>
|
||||
</ProviderComposer>
|
||||
)
|
||||
const ArticleElementResizeObserver = () => {
|
||||
const setSize = useSetWrappedElementSize()
|
||||
const setPos = useSetElementPosition()
|
||||
const $article = useWrappedElement()
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!$article) return
|
||||
const { height, width, x, y } = $article.getBoundingClientRect()
|
||||
setSize({ h: height, w: width })
|
||||
setPos({ x, y })
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0]
|
||||
const { height, width, x, y } = entry.contentRect
|
||||
setSize({ h: height, w: width })
|
||||
setPos({ x, y })
|
||||
})
|
||||
observer.observe($article)
|
||||
return () => {
|
||||
observer.unobserve($article)
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [$article])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const Content: Component<WrappedElementProviderProps> = memo(
|
||||
({ children, className, boundingDetection, as = "div" }) => {
|
||||
const setElement = useSetWrappedElement()
|
||||
|
||||
const As = as as any
|
||||
return (
|
||||
<As className={cn("relative", className)} ref={setElement}>
|
||||
{boundingDetection && <BoundingDetection bounding="start" />}
|
||||
{children}
|
||||
{boundingDetection && <BoundingDetection bounding="end" />}
|
||||
</As>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Content.displayName = "ArticleElementProviderContent"
|
||||
|
||||
const BoundingDetection: Component<{
|
||||
bounding: "start" | "end"
|
||||
}> = ({ bounding }) => {
|
||||
const endSetter = useSetIsEOfElement()
|
||||
const startSetter = useSetIsSOfElement()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (!ref.current) return
|
||||
const $el = ref.current
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
|
||||
if (bounding === "start") startSetter(entry.isIntersecting)
|
||||
else endSetter(entry.isIntersecting)
|
||||
},
|
||||
{
|
||||
rootMargin: "0px 0px 0px 0px",
|
||||
},
|
||||
)
|
||||
|
||||
observer.observe($el)
|
||||
return () => {
|
||||
observer.unobserve($el)
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
|
||||
export {
|
||||
useIsEoFWrappedElement,
|
||||
useIsSoFWrappedElement,
|
||||
useSetWrappedElement,
|
||||
useWrappedElement,
|
||||
useWrappedElementPosition,
|
||||
useWrappedElementSize,
|
||||
}
|
||||
Loading…
Reference in New Issue