fix: align entry details column layout with ai panel states
This commit is contained in:
parent
36bc50b1c0
commit
878ad81a7d
|
|
@ -0,0 +1,17 @@
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
|
||||
import { AIChatPanelStyle, useAIChatPanelStyle, useAIPanelVisibility } from "~/atoms/settings/ai"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
|
||||
export const useShowEntryDetailsColumn = () => {
|
||||
const { view } = useRouteParamsSelector((s) => ({
|
||||
view: s.view,
|
||||
}))
|
||||
const aiPanelStyle = useAIChatPanelStyle()
|
||||
const isAIPanelVisible = useAIPanelVisibility()
|
||||
|
||||
return (
|
||||
view === FeedViewType.Articles &&
|
||||
(aiPanelStyle === AIChatPanelStyle.Floating || !isAIPanelVisible)
|
||||
)
|
||||
}
|
||||
|
|
@ -3,20 +3,23 @@ import { PanelSplitter } from "@follow/components/ui/divider/index.js"
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
import { defaultUISettings } from "@follow/shared/settings/defaults"
|
||||
import { cn } from "@follow/utils"
|
||||
import { isSafari } from "@follow/utils/utils"
|
||||
import { AnimatePresence } from "motion/react"
|
||||
import { memo, startTransition, useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import { useResizable } from "react-resizable-layout"
|
||||
|
||||
import { AIChatPanelStyle, useAIChatPanelStyle, useAIPanelVisibility } from "~/atoms/settings/ai"
|
||||
import { useGeneralSettingKey } from "~/atoms/settings/general"
|
||||
import { getUISettings, setUISetting } from "~/atoms/settings/ui"
|
||||
import { getUISettings, setUISetting, useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { setSubscriptionColumnApronNode, useSubscriptionEntryPlaneVisible } from "~/atoms/sidebar"
|
||||
import { m } from "~/components/common/Motion"
|
||||
import { ROUTE_ENTRY_PENDING } from "~/constants"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
import { useShowEntryDetailsColumn } from "~/hooks/biz/useShowEntryDetailsColumn"
|
||||
import { AIChatRoot } from "~/modules/ai-chat/components/layouts/AIChatRoot"
|
||||
import { AIChatLayout } from "~/modules/app-layout/ai/AIChatLayout"
|
||||
import { AIIndicator } from "~/modules/app-layout/ai/AISplineButton"
|
||||
import { EntryContentPlaceholder } from "~/modules/app-layout/entry-content/EntryContentPlaceholder"
|
||||
import { EntryColumn } from "~/modules/entry-column"
|
||||
import { EntryPlaneToolbar } from "~/modules/entry-column/components/EntryPlaneToolbar"
|
||||
import { EntrySubscriptionList } from "~/modules/entry-column/EntrySubscriptionList"
|
||||
|
|
@ -64,6 +67,40 @@ const AIEnhancedTimelineLayoutImpl = () => {
|
|||
entryId: s.entryId,
|
||||
}))
|
||||
const realEntryId = entryId === ROUTE_ENTRY_PENDING ? "" : entryId
|
||||
const showEntryDetailsColumn = useShowEntryDetailsColumn()
|
||||
|
||||
const layoutContainerRef = useRef<HTMLDivElement>(null)
|
||||
const feedColumnWidth = useUISettingKey("feedColWidth")
|
||||
|
||||
// Timeline column resizable configuration (used when entry details column is visible)
|
||||
const entryColumnInitialWidth = useMemo(() => getUISettings().entryColWidth, [])
|
||||
const timelineMaxWidth = useMemo(() => {
|
||||
if (typeof window === "undefined") return 600
|
||||
return Math.max((window.innerWidth - feedColumnWidth) / 2, 600)
|
||||
}, [feedColumnWidth])
|
||||
const timelineStartDragPosition = useRef(0)
|
||||
const {
|
||||
position: timelineColumnWidth,
|
||||
separatorProps: timelineSeparatorProps,
|
||||
isDragging: isTimelineDragging,
|
||||
separatorCursor: timelineSeparatorCursor,
|
||||
setPosition: setTimelineColumnWidth,
|
||||
} = useResizable({
|
||||
axis: "x",
|
||||
min: isSafari() ? 356 : 300,
|
||||
max: timelineMaxWidth,
|
||||
initial: entryColumnInitialWidth,
|
||||
containerRef: layoutContainerRef as React.RefObject<HTMLElement>,
|
||||
onResizeStart({ position }) {
|
||||
timelineStartDragPosition.current = position
|
||||
},
|
||||
onResizeEnd({ position }) {
|
||||
if (position === timelineStartDragPosition.current) return
|
||||
setUISetting("entryColWidth", position)
|
||||
// TODO: Remove this after useMeasure can get bounds in time
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
},
|
||||
})
|
||||
|
||||
// AI chat resizable panel configuration
|
||||
const isAllView = view === FeedViewType.All
|
||||
|
|
@ -88,19 +125,25 @@ const AIEnhancedTimelineLayoutImpl = () => {
|
|||
|
||||
// Calculate initial width depending on view
|
||||
const initialAiWidth = useMemo(() => resolvePreferredWidth(), [resolvePreferredWidth])
|
||||
const startDragPosition = useRef(0)
|
||||
const aiPanelStartDragPosition = useRef(0)
|
||||
|
||||
const { position, separatorProps, isDragging, separatorCursor, setPosition } = useResizable({
|
||||
const {
|
||||
position: aiPanelWidth,
|
||||
separatorProps: aiSeparatorProps,
|
||||
isDragging: isAiPanelDragging,
|
||||
separatorCursor: aiSeparatorCursor,
|
||||
setPosition: setAiPanelWidth,
|
||||
} = useResizable({
|
||||
axis: "x",
|
||||
min: minWidth,
|
||||
max: maxWidth,
|
||||
initial: initialAiWidth,
|
||||
reverse: true,
|
||||
onResizeStart({ position }) {
|
||||
startDragPosition.current = position
|
||||
aiPanelStartDragPosition.current = position
|
||||
},
|
||||
onResizeEnd({ position }) {
|
||||
if (position === startDragPosition.current) return
|
||||
if (position === aiPanelStartDragPosition.current) return
|
||||
// Persist per-view width
|
||||
setUISetting(isAllView ? "aiColWidthAll" : "aiColWidth", position)
|
||||
// TODO: Remove this after useMeasure can get bounds in time
|
||||
|
|
@ -111,54 +154,102 @@ const AIEnhancedTimelineLayoutImpl = () => {
|
|||
// When view changes, switch to the saved width for that view
|
||||
useEffect(() => {
|
||||
const width = resolvePreferredWidth()
|
||||
setPosition(width)
|
||||
setAiPanelWidth(width)
|
||||
// Trigger layout update
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}, [resolvePreferredWidth])
|
||||
|
||||
const showCompactTimelineColumn = useGeneralSettingKey("showCompactTimelineInSub")
|
||||
return (
|
||||
<div className="relative flex min-w-0 grow">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full min-w-[300px] flex-1",
|
||||
aiPanelStyle === AIChatPanelStyle.Fixed && "border-r",
|
||||
)}
|
||||
>
|
||||
<div ref={layoutContainerRef} className="relative flex min-w-0 grow">
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<AppLayoutGridContainerProvider>
|
||||
<div className="relative h-full">
|
||||
{/* Entry list - always rendered to prevent animation */}
|
||||
<EntryColumn key="entry-list" />
|
||||
<AnimatePresence>
|
||||
{realEntryId && (
|
||||
<m.div
|
||||
className="absolute inset-x-0 top-0 z-10"
|
||||
initial={{ translateY: "-50px", opacity: 0 }}
|
||||
animate={{ translateY: 0, opacity: 1 }}
|
||||
exit={{ translateY: "-50px", opacity: 0 }}
|
||||
transition={Spring.smooth(0.3)}
|
||||
>
|
||||
<AIEntryHeader entryId={realEntryId} />
|
||||
</m.div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-y-0 left-0 min-w-[300px]",
|
||||
showEntryDetailsColumn ? "border-r will-change-[width]" : "right-0",
|
||||
aiPanelStyle === AIChatPanelStyle.Fixed && !showEntryDetailsColumn && "border-r",
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* Entry content overlay with exit animation */}
|
||||
<AnimatePresence>
|
||||
{realEntryId && (
|
||||
<div className="pointer-events-none absolute inset-0 z-[9] flex flex-col overflow-hidden">
|
||||
<m.div
|
||||
lcpOptimization
|
||||
initial={{ translateY: "50px", opacity: 0, scale: 0.98 }}
|
||||
animate={{ translateY: 0, opacity: 1, scale: 1 }}
|
||||
exit={{ translateY: "50px", opacity: 0, scale: 0.98 }}
|
||||
transition={Spring.smooth(0.3)}
|
||||
className="bg-theme-background pointer-events-auto relative flex h-0 flex-1 flex-col"
|
||||
>
|
||||
<EntryContent entryId={realEntryId} className="h-full" />
|
||||
</m.div>
|
||||
style={showEntryDetailsColumn ? { width: timelineColumnWidth } : undefined}
|
||||
>
|
||||
<div className="relative h-full">
|
||||
{/* Entry list - always rendered to prevent animation */}
|
||||
<EntryColumn key="entry-list" />
|
||||
{!showEntryDetailsColumn && (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{realEntryId && (
|
||||
<m.div
|
||||
className="absolute inset-x-0 top-0 z-10"
|
||||
initial={{ translateY: "-50px", opacity: 0 }}
|
||||
animate={{ translateY: 0, opacity: 1 }}
|
||||
exit={{ translateY: "-50px", opacity: 0 }}
|
||||
transition={Spring.smooth(0.3)}
|
||||
>
|
||||
<AIEntryHeader entryId={realEntryId} />
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* Entry content overlay with exit animation */}
|
||||
<AnimatePresence>
|
||||
{realEntryId && (
|
||||
<div className="pointer-events-none absolute inset-0 z-[9] flex flex-col overflow-hidden">
|
||||
<m.div
|
||||
lcpOptimization
|
||||
initial={{ translateY: "50px", opacity: 0, scale: 0.98 }}
|
||||
animate={{ translateY: 0, opacity: 1, scale: 1 }}
|
||||
exit={{ translateY: "50px", opacity: 0, scale: 0.98 }}
|
||||
transition={Spring.smooth(0.3)}
|
||||
className="bg-theme-background pointer-events-auto relative flex h-0 flex-1 flex-col"
|
||||
>
|
||||
<EntryContent entryId={realEntryId} className="h-full" />
|
||||
</m.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showEntryDetailsColumn && (
|
||||
<>
|
||||
<div className="absolute inset-y-0 z-[2]" style={{ left: timelineColumnWidth }}>
|
||||
<PanelSplitter
|
||||
{...timelineSeparatorProps}
|
||||
cursor={timelineSeparatorCursor}
|
||||
isDragging={isTimelineDragging}
|
||||
onDoubleClick={() => {
|
||||
setUISetting("entryColWidth", defaultUISettings.entryColWidth)
|
||||
setTimelineColumnWidth(defaultUISettings.entryColWidth)
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div
|
||||
className="bg-theme-background absolute inset-y-0 right-0 flex min-w-0 flex-1 flex-col overflow-hidden"
|
||||
style={{ left: timelineColumnWidth }}
|
||||
>
|
||||
{realEntryId ? (
|
||||
<>
|
||||
<div className="absolute inset-x-0 top-0 z-10">
|
||||
<AIEntryHeader entryId={realEntryId} />
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 z-[9] flex flex-col overflow-hidden">
|
||||
<div className="bg-theme-background pointer-events-auto relative flex h-0 flex-1 flex-col">
|
||||
<EntryContent entryId={realEntryId} className="h-full" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center px-8">
|
||||
<EntryContentPlaceholder />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AppLayoutGridContainerProvider>
|
||||
</div>
|
||||
|
|
@ -167,22 +258,25 @@ const AIEnhancedTimelineLayoutImpl = () => {
|
|||
{aiSidebarVisible && (
|
||||
<>
|
||||
<PanelSplitter
|
||||
{...separatorProps}
|
||||
cursor={separatorCursor}
|
||||
isDragging={isDragging}
|
||||
{...aiSeparatorProps}
|
||||
cursor={aiSeparatorCursor}
|
||||
isDragging={isAiPanelDragging}
|
||||
onDoubleClick={() => {
|
||||
const resetWidth = isAllView
|
||||
? getHalfScreenWidth()
|
||||
: clampWidth(defaultUISettings.aiColWidth)
|
||||
setUISetting(isAllView ? "aiColWidthAll" : "aiColWidth", resetWidth)
|
||||
setPosition(resetWidth)
|
||||
setAiPanelWidth(resetWidth)
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}}
|
||||
/>
|
||||
<AIChatLayout
|
||||
key="ai-chat-layout"
|
||||
style={
|
||||
{ width: position, "--ai-chat-layout-width": `${position}px` } as React.CSSProperties
|
||||
{
|
||||
width: aiPanelWidth,
|
||||
"--ai-chat-layout-width": `${aiPanelWidth}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { RelativeDay } from "~/components/ui/datetime"
|
|||
import { IconScaleTransition } from "~/components/ux/transition/icon"
|
||||
import { useFeature } from "~/hooks/biz/useFeature"
|
||||
import { getRouteParams, useRouteParams } from "~/hooks/biz/useRouteParams"
|
||||
import { useShowEntryDetailsColumn } from "~/hooks/biz/useShowEntryDetailsColumn"
|
||||
|
||||
import { markAllByRoute } from "../hooks/useMarkAll"
|
||||
|
||||
|
|
@ -43,7 +44,9 @@ const useParseDate = (date: string) =>
|
|||
const dateItemclassName = tw`relative flex items-center text-sm lg:text-base gap-1 px-4 font-bold text-text h-7`
|
||||
export const DateItem = memo(({ date, view, isSticky }: DateItemProps) => {
|
||||
const aiEnabled = useFeature("ai")
|
||||
if (view === FeedViewType.SocialMedia || aiEnabled) {
|
||||
const showEntryDetailsColumn = useShowEntryDetailsColumn()
|
||||
|
||||
if (view === FeedViewType.SocialMedia || (aiEnabled && !showEntryDetailsColumn)) {
|
||||
return <SocialMediaDateItem date={date} className={dateItemclassName} isSticky={isSticky} />
|
||||
}
|
||||
return <UniversalDateItem date={date} className={dateItemclassName} isSticky={isSticky} />
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { cn } from "@follow/utils"
|
|||
import { ErrorBoundary } from "@sentry/react"
|
||||
import { useCallback, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { AIChatPanelStyle, useAIChatPanelStyle } from "~/atoms/settings/ai"
|
||||
import { AIChatPanelStyle, useAIChatPanelStyle, useAIPanelVisibility } from "~/atoms/settings/ai"
|
||||
import { useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { ShadowDOM } from "~/components/common/ShadowDOM"
|
||||
import type { TocRef } from "~/components/ui/markdown/components/Toc"
|
||||
|
|
@ -66,8 +66,9 @@ export const ArticleLayout: React.FC<EntryLayoutProps> = ({
|
|||
}, [removeBlock])
|
||||
|
||||
const aiChatPanelStyle = useAIChatPanelStyle()
|
||||
const isAIPanelVisible = useAIPanelVisibility()
|
||||
|
||||
const shouldShowAISummary = aiChatPanelStyle === AIChatPanelStyle.Floating
|
||||
const shouldShowAISummary = aiChatPanelStyle === AIChatPanelStyle.Floating || !isAIPanelVisible
|
||||
if (!entry) return null
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in New Issue