UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

439 lines (436 loc) 12.5 kB
'use client'; import FlexBasic_default from "../Flex/FlexBasic.mjs"; import Center_default from "../Flex/Center.mjs"; import Icon_default from "../Icon/Icon.mjs"; import { styles } from "./style.mjs"; import { memo, useCallback, useEffect, useMemo, useReducer, useRef } from "react"; import { jsx, jsxs } from "react/jsx-runtime"; import { cx } from "antd-style"; import useMergeState from "use-merge-value"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { useHover } from "ahooks"; import { Resizable } from "re-resizable"; //#region src/DraggableSideNav/DraggableSideNav.tsx const DEFAULT_MIN_WIDTH = 64; const DEFAULT_EXPAND = true; const DEFAULT_EXPANDED_WIDTH = 280; const ANIMATION_DURATION = 300; const COLLAPSE_ANIMATION_DELAY = 200; const RESIZE_DISABLED = { bottom: false, bottomLeft: false, bottomRight: false, left: false, right: false, top: false, topLeft: false, topRight: false }; function sideNavReducer(state, action) { switch (action.type) { case "START_RESIZE": return { ...state, isResizing: true }; case "STOP_RESIZE": return { ...state, isResizing: false }; case "START_ANIMATION": return { ...state, isAnimating: true }; case "STOP_ANIMATION": return { ...state, isAnimating: false }; case "SET_WIDTH": return { ...state, internalWidth: action.payload }; case "SET_EXPANDED_WIDTH": return { ...state, expandedWidth: action.payload }; case "SET_RENDER_EXPAND": return { ...state, renderExpand: action.payload }; case "ANIMATE_EXPAND": return { ...state, internalWidth: action.payload, renderExpand: true }; case "ANIMATE_COLLAPSE": return { ...state, internalWidth: action.payload }; default: return state; } } const DraggableSideNav = memo(({ body, className, classNames, defaultExpand = DEFAULT_EXPAND, defaultWidth, expand, expandable = true, footer, header, maxWidth, minWidth = DEFAULT_MIN_WIDTH, onExpandChange, onWidthChange, onWidthDragging, placement = "left", resizable = true, showBorder = true, showHandle = true, showHandleWhenCollapsed = false, showHandleHighlight = false, backgroundColor, styles: customStyles, width, ...rest }) => { const cssVariables = useMemo(() => ({ "--draggable-side-nav-bg": backgroundColor || "" }), [backgroundColor]); const ref = useRef(null); const isHovering = useHover(ref); const [isExpand, setIsExpand] = useMergeState(defaultExpand, { onChange: onExpandChange, value: expand }); const animationTimeoutRef = useRef(void 0); const collapseTimeoutRef = useRef(void 0); const computedDefaultExpandedWidth = useMemo(() => defaultWidth || DEFAULT_EXPANDED_WIDTH, [defaultWidth]); const [state, dispatch] = useReducer(sideNavReducer, { expandedWidth: width ?? computedDefaultExpandedWidth, internalWidth: isExpand ? width ?? computedDefaultExpandedWidth : minWidth, isAnimating: false, isResizing: false, renderExpand: isExpand }); const collapseThreshold = useMemo(() => { return minWidth + (state.expandedWidth - minWidth) / 3; }, [minWidth, state.expandedWidth]); const toggleExpand = useCallback(() => { if (!expandable) return; if (state.isAnimating || state.isResizing) return; if (animationTimeoutRef.current) clearTimeout(animationTimeoutRef.current); dispatch({ type: "START_ANIMATION" }); setIsExpand(!isExpand); animationTimeoutRef.current = setTimeout(() => { dispatch({ type: "STOP_ANIMATION" }); }, ANIMATION_DURATION); }, [ expandable, isExpand, setIsExpand, state.isAnimating, state.isResizing ]); const prevExpandRef = useRef(isExpand); useEffect(() => { if (prevExpandRef.current !== isExpand && !state.isResizing && !state.isAnimating) { if (animationTimeoutRef.current) clearTimeout(animationTimeoutRef.current); dispatch({ type: "START_ANIMATION" }); animationTimeoutRef.current = setTimeout(() => { dispatch({ type: "STOP_ANIMATION" }); }, ANIMATION_DURATION); prevExpandRef.current = isExpand; } }, [ isExpand, state.isResizing, state.isAnimating ]); useEffect(() => { if (state.isAnimating) { const rafId = requestAnimationFrame(() => { if (isExpand) dispatch({ payload: state.expandedWidth, type: "ANIMATE_EXPAND" }); else { dispatch({ payload: minWidth, type: "ANIMATE_COLLAPSE" }); if (collapseTimeoutRef.current) clearTimeout(collapseTimeoutRef.current); collapseTimeoutRef.current = setTimeout(() => { dispatch({ payload: false, type: "SET_RENDER_EXPAND" }); }, COLLAPSE_ANIMATION_DELAY); } }); return () => { cancelAnimationFrame(rafId); }; } }, [ isExpand, state.isAnimating, minWidth, state.expandedWidth ]); const prevIsResizingRef = useRef(state.isResizing); useEffect(() => { const wasResizing = prevIsResizingRef.current; prevIsResizingRef.current = state.isResizing; if (wasResizing && !state.isResizing && !state.isAnimating) dispatch({ payload: isExpand, type: "SET_RENDER_EXPAND" }); }, [ isExpand, state.isAnimating, state.isResizing ]); useEffect(() => { if (width !== void 0 && !state.isResizing && !state.isAnimating) { dispatch({ payload: width, type: "SET_EXPANDED_WIDTH" }); if (isExpand) dispatch({ payload: width, type: "SET_WIDTH" }); } }, [ width, state.isResizing, state.isAnimating, isExpand ]); const currentBody = useMemo(() => { return body(state.renderExpand); }, [body, state.renderExpand]); const currentHeader = useMemo(() => { return typeof header === "function" ? header(state.renderExpand) : header; }, [header, state.renderExpand]); const currentFooter = useMemo(() => { return typeof footer === "function" ? footer(state.renderExpand) : footer; }, [footer, state.renderExpand]); const handleResize = useCallback((_, __, ref$1, delta) => { const currentWidth = ref$1.offsetWidth; dispatch({ payload: currentWidth, type: "SET_WIDTH" }); onWidthDragging?.(delta, currentWidth); }, [onWidthDragging]); const handleResizeStart = useCallback(() => { dispatch({ type: "START_RESIZE" }); }, []); const handleResizeStop = useCallback((_, __, ref$1, delta) => { dispatch({ type: "STOP_RESIZE" }); const currentWidth = ref$1.offsetWidth; if (animationTimeoutRef.current) clearTimeout(animationTimeoutRef.current); if (expandable) { const shouldCollapse = currentWidth <= minWidth || currentWidth < collapseThreshold; if (shouldCollapse || !isExpand && currentWidth > minWidth && currentWidth >= collapseThreshold) { dispatch({ type: "START_ANIMATION" }); if (shouldCollapse) { setIsExpand(false); dispatch({ payload: minWidth, type: "SET_WIDTH" }); } else { setIsExpand(true); dispatch({ payload: currentWidth, type: "SET_EXPANDED_WIDTH" }); dispatch({ payload: currentWidth, type: "SET_WIDTH" }); } animationTimeoutRef.current = setTimeout(() => { dispatch({ type: "STOP_ANIMATION" }); }, ANIMATION_DURATION); } else if (isExpand) { dispatch({ payload: currentWidth, type: "SET_EXPANDED_WIDTH" }); dispatch({ payload: currentWidth, type: "SET_WIDTH" }); } } else { dispatch({ payload: currentWidth, type: "SET_EXPANDED_WIDTH" }); dispatch({ payload: currentWidth, type: "SET_WIDTH" }); } onWidthChange?.(delta, currentWidth); }, [ expandable, minWidth, collapseThreshold, isExpand, onWidthChange, setIsExpand ]); const ArrowIcon = useMemo(() => { if (placement === "left") return ChevronLeft; return ChevronRight; }, [placement]); const handleRootStyle = useMemo(() => ({ display: "flex", opacity: !isExpand && showHandleWhenCollapsed ? 1 : isHovering ? 1 : 0, transition: "opacity 0.25s ease" }), [ isExpand, showHandleWhenCollapsed, isHovering ]); const handleCenterStyle = useMemo(() => ({ ...customStyles?.handle, cursor: "pointer" }), [customStyles?.handle]); const handleIconWrapperStyle = useMemo(() => ({ marginLeft: placement === "right" ? 4 : 0, marginRight: placement === "left" ? 4 : 0, transform: isExpand ? "rotate(0deg)" : "rotate(180deg)", transition: `transform ${COLLAPSE_ANIMATION_DELAY} ease` }), [placement, isExpand]); const handle = useMemo(() => showHandle && expandable && /* @__PURE__ */ jsx("div", { className: cx(styles.toggleRoot, placement === "left" ? styles.toggleLeft : styles.toggleRight), style: handleRootStyle, children: /* @__PURE__ */ jsx(Center_default, { className: classNames?.handle, onClick: toggleExpand, style: handleCenterStyle, children: /* @__PURE__ */ jsx("div", { style: handleIconWrapperStyle, children: /* @__PURE__ */ jsx(Icon_default, { className: styles.handlerIcon, icon: ArrowIcon, size: 16 }) }) }) }), [ showHandle, expandable, styles.toggleRoot, styles.toggleLeft, styles.toggleRight, styles.handlerIcon, placement, handleRootStyle, classNames?.handle, toggleExpand, handleCenterStyle, handleIconWrapperStyle, ArrowIcon, cx ]); const sizeConfig = useMemo(() => { return { maxWidth, minWidth, size: { height: "100%", width: state.internalWidth } }; }, [ state.internalWidth, minWidth, maxWidth ]); const resizeEnable = useMemo(() => { if (!resizable) return RESIZE_DISABLED; return { bottom: false, bottomLeft: false, bottomRight: false, left: placement === "right", right: placement === "left", top: false, topLeft: false, topRight: false }; }, [resizable, placement]); const handleClasses = useMemo(() => ({ [placement === "left" ? "right" : "left"]: cx(styles.resizeHandle, showHandleHighlight && styles.resizeHandleHighlight, placement === "left" ? styles.resizeHandleLeft : styles.resizeHandleRight) }), [ placement, styles, showHandleHighlight, cx ]); const containerStyle = useMemo(() => ({ ...customStyles?.container, ...rest.style, transition: state.isResizing ? "none" : state.isAnimating ? `width ${ANIMATION_DURATION}ms cubic-bezier(0.22, 1, 0.36, 1)` : "none" }), [ customStyles?.container, rest.style, state.isResizing, state.isAnimating ]); const containerClassName = useMemo(() => cx(styles.container, classNames?.container, className), [ cx, styles.container, classNames?.container, className ]); const contentClassName = useMemo(() => cx(showBorder ? styles.contentContainer : styles.contentContainerNoBorder, styles.menuOverride, classNames?.content), [ cx, styles.contentContainer, styles.contentContainerNoBorder, styles.menuOverride, classNames?.content, showBorder ]); const headerClassName = useMemo(() => cx(styles.header, classNames?.header), [ cx, styles.header, classNames?.header ]); const bodyClassName = useMemo(() => cx(styles.body, classNames?.body), [ cx, styles.body, classNames?.body ]); const footerClassName = useMemo(() => cx(styles.footer, classNames?.footer), [ cx, styles.footer, classNames?.footer ]); useEffect(() => { return () => { if (animationTimeoutRef.current) clearTimeout(animationTimeoutRef.current); if (collapseTimeoutRef.current) clearTimeout(collapseTimeoutRef.current); }; }, []); return /* @__PURE__ */ jsx("aside", { ref, children: /* @__PURE__ */ jsxs(Resizable, { ...sizeConfig, className: containerClassName, enable: resizeEnable, handleClasses, onResize: handleResize, onResizeStart: handleResizeStart, onResizeStop: handleResizeStop, style: containerStyle, children: [handle, /* @__PURE__ */ jsxs(FlexBasic_default, { className: contentClassName, style: { ...cssVariables, ...customStyles?.content }, children: [ currentHeader && /* @__PURE__ */ jsx("div", { className: headerClassName, style: customStyles?.header, children: currentHeader }), /* @__PURE__ */ jsx("div", { className: bodyClassName, style: customStyles?.body, children: currentBody }), currentFooter && /* @__PURE__ */ jsx("div", { className: footerClassName, style: customStyles?.footer, children: currentFooter }) ] })] }) }); }); DraggableSideNav.displayName = "DraggableSideNav"; var DraggableSideNav_default = DraggableSideNav; //#endregion export { DraggableSideNav_default as default }; //# sourceMappingURL=DraggableSideNav.mjs.map