diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index f508b65e..a9f9aeb3 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -523,6 +523,7 @@ export const ChatContainer: React.FC = () => { className="absolute inset-0 overflow-y-auto overflow-x-hidden z-0 chat-scroll overlay-scrollbar-target" ref={scrollRef} observeMutations={false} + hideTopShadow={isMobile} data-scroll-shadow="true" data-scrollbar="chat" > diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 2d5ff587..8afddffd 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -2319,7 +2319,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo )} - {/* Mobile Session Status Bar - 在输入框上方 */} + {/* Mobile Session Status Bar - above input */} {isMobile && } diff --git a/packages/ui/src/components/chat/ChatMessage.tsx b/packages/ui/src/components/chat/ChatMessage.tsx index 9c216c35..74cac807 100644 --- a/packages/ui/src/components/chat/ChatMessage.tsx +++ b/packages/ui/src/components/chat/ChatMessage.tsx @@ -909,7 +909,7 @@ const ChatMessage: React.FC = ({
= ({ onRenderEarlier, scrollToBottom, }) => { + const { isMobile } = useDeviceInfo(); React.useEffect(() => { if (permissions.length === 0 && questions.length === 0) { return; @@ -641,8 +643,8 @@ const MessageList: React.FC = ({
)} - {/* Bottom spacer - always 10% of viewport height */} - - {/* 右抽屉(Git) */} + {/* Right drawer (Git) */} { - {/* 主内容区(固定) */} - { )} - + {/* Mobile multi-run launcher: full screen */} {isMultiRunLauncherOpen && ( diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 9d93f438..82db99cf 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -134,7 +134,6 @@ export const OpenChamberVisualSettings: React.FC } = useThemeSystem(); const [themesReloading, setThemesReloading] = React.useState(false); - const lightThemes = React.useMemo( () => availableThemes .filter((theme) => theme.metadata.variant === 'light') diff --git a/packages/ui/src/components/ui/ScrollShadow.tsx b/packages/ui/src/components/ui/ScrollShadow.tsx index 1ce56f6c..31f36426 100644 --- a/packages/ui/src/components/ui/ScrollShadow.tsx +++ b/packages/ui/src/components/ui/ScrollShadow.tsx @@ -5,6 +5,7 @@ export type ScrollShadowProps = React.HTMLAttributes & { offset?: number; size?: number; isEnabled?: boolean; + hideTopShadow?: boolean; hideBottomShadow?: boolean; observeMutations?: boolean; onVisibilityChange?: (state: "both" | "none" | "top" | "bottom" | "left" | "right") => void; @@ -29,6 +30,7 @@ export const ScrollShadow = React.forwardRef( offset = 0, size = 48, isEnabled = true, + hideTopShadow = false, hideBottomShadow = false, observeMutations = true, onVisibilityChange, @@ -91,18 +93,20 @@ export const ScrollShadow = React.forwardRef( ? el.scrollTop + el.clientHeight + offset < el.scrollHeight : el.scrollLeft + el.clientWidth + offset < el.scrollWidth; + const effectiveHasBefore = hideTopShadow && orientation === "vertical" ? false : hasBefore; + if (hideBottomShadow && orientation === "vertical") { hasAfter = false; } - setAttributes(el, hasBefore, hasAfter, orientation === "vertical" ? "top" : "left", orientation === "vertical" ? "bottom" : "right"); + setAttributes(el, effectiveHasBefore, hasAfter, orientation === "vertical" ? "top" : "left", orientation === "vertical" ? "bottom" : "right"); - const next = hasBefore && hasAfter ? "both" : hasBefore ? (orientation === "vertical" ? "top" : "left") : hasAfter ? (orientation === "vertical" ? "bottom" : "right") : "none"; + const next = effectiveHasBefore && hasAfter ? "both" : effectiveHasBefore ? (orientation === "vertical" ? "top" : "left") : hasAfter ? (orientation === "vertical" ? "bottom" : "right") : "none"; if (next !== visibleRef.current) { visibleRef.current = next; onVisibilityChange?.(next); } - }, [clearAttributes, hideBottomShadow, isEnabled, offset, onVisibilityChange, orientation, setAttributes]); + }, [clearAttributes, hideTopShadow, hideBottomShadow, isEnabled, offset, onVisibilityChange, orientation, setAttributes]); React.useEffect(() => { const el = internalRef.current; diff --git a/packages/ui/src/hooks/useDrawerSwipe.ts b/packages/ui/src/hooks/useDrawerSwipe.ts index 5b919c0b..a1923a1d 100644 --- a/packages/ui/src/hooks/useDrawerSwipe.ts +++ b/packages/ui/src/hooks/useDrawerSwipe.ts @@ -2,8 +2,23 @@ import React from 'react'; import { animate } from 'motion/react'; import { useDrawer } from '@/contexts/DrawerContext'; -export function useDrawerSwipe() { +type DrawerSwipeOptions = { + edgeSide?: 'left' | 'right'; + strictHorizontalIntent?: boolean; + horizontalIntentRatio?: number; + activationDistance?: number; + onlyWhenClosed?: boolean; +}; + +export function useDrawerSwipe(options: DrawerSwipeOptions = {}) { const drawer = useDrawer(); + const { + edgeSide, + strictHorizontalIntent = false, + horizontalIntentRatio = 1.35, + activationDistance = 30, + onlyWhenClosed = false, + } = options; const touchStartXRef = React.useRef(0); const touchStartYRef = React.useRef(0); const isHorizontalSwipeRef = React.useRef(null); @@ -24,30 +39,50 @@ export function useDrawerSwipe() { if (isHorizontalSwipeRef.current === null) { if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { - isHorizontalSwipeRef.current = Math.abs(deltaX) > Math.abs(deltaY); + if (strictHorizontalIntent) { + isHorizontalSwipeRef.current = Math.abs(deltaX) > Math.abs(deltaY) * horizontalIntentRatio; + } else { + isHorizontalSwipeRef.current = Math.abs(deltaX) > Math.abs(deltaY); + } } } if (isHorizontalSwipeRef.current === true) { - e.preventDefault(); + if (onlyWhenClosed && (drawer.leftDrawerOpen || drawer.rightDrawerOpen)) { + return; + } const leftDrawerWidthPx = drawer.leftDrawerWidth.current || window.innerWidth * 0.85; const rightDrawerWidthPx = drawer.rightDrawerWidth.current || window.innerWidth * 0.85; if (isDraggingDrawerRef.current === null) { - if (drawer.leftDrawerOpen && deltaX > 10) { + if (!edgeSide && drawer.leftDrawerOpen && deltaX > 10) { isDraggingDrawerRef.current = 'left'; - } else if (drawer.rightDrawerOpen && deltaX < -10) { + } else if (!edgeSide && drawer.rightDrawerOpen && deltaX < -10) { isDraggingDrawerRef.current = 'right'; } else if (!drawer.leftDrawerOpen && !drawer.rightDrawerOpen) { - if (deltaX > 30) { + if (edgeSide === 'left') { + if (deltaX > activationDistance) { + isDraggingDrawerRef.current = 'left'; + } + } else if (edgeSide === 'right') { + if (deltaX < -activationDistance) { + isDraggingDrawerRef.current = 'right'; + } + } else if (deltaX > activationDistance) { isDraggingDrawerRef.current = 'left'; - } else if (deltaX < -30) { + } else if (deltaX < -activationDistance) { isDraggingDrawerRef.current = 'right'; } } } + if (!isDraggingDrawerRef.current) { + return; + } + + e.preventDefault(); + if (isDraggingDrawerRef.current === 'left') { if (drawer.leftDrawerOpen) { const progress = Math.max(0, Math.min(1, deltaX / leftDrawerWidthPx)); @@ -68,7 +103,7 @@ export function useDrawerSwipe() { } } } - }, [drawer]); + }, [activationDistance, drawer, edgeSide, horizontalIntentRatio, onlyWhenClosed, strictHorizontalIntent]); const handleTouchEnd = React.useCallback((e: React.TouchEvent) => { if (isHorizontalSwipeRef.current !== true) return;