diff --git a/.dumi/pages/index/components/MainBanner.tsx b/.dumi/pages/index/components/MainBanner.tsx index 8eaa6a8c..ea11533e 100644 --- a/.dumi/pages/index/components/MainBanner.tsx +++ b/.dumi/pages/index/components/MainBanner.tsx @@ -1,11 +1,12 @@ import { createStyles } from 'antd-style'; import classnames from 'classnames'; -import { Link, useLocation } from 'dumi'; +import { useLocation } from 'dumi'; import React from 'react'; import { Button } from 'antd'; import useLocale from '../../../hooks/useLocale'; import useLottie from '../../../hooks/useLottie'; +import Link from '../../../theme/common/Link'; import { getLocalizedPathname, isZhCN } from '../../../theme/utils'; import Container from '../common/Container'; import SiteContext from './SiteContext'; diff --git a/.dumi/theme/builtins/ComponentOverview/index.tsx b/.dumi/theme/builtins/ComponentOverview/index.tsx index e62a43b5..b730f9c5 100644 --- a/.dumi/theme/builtins/ComponentOverview/index.tsx +++ b/.dumi/theme/builtins/ComponentOverview/index.tsx @@ -243,7 +243,7 @@ const Overview: React.FC = () => { {cardContent} ) : ( - + {cardContent} ); diff --git a/.dumi/theme/builtins/DemoWrapper/index.tsx b/.dumi/theme/builtins/DemoWrapper/index.tsx index 15816017..92f19f18 100644 --- a/.dumi/theme/builtins/DemoWrapper/index.tsx +++ b/.dumi/theme/builtins/DemoWrapper/index.tsx @@ -1,9 +1,9 @@ -import React, { useContext } from 'react'; import { BugOutlined, CodeOutlined, ExperimentOutlined } from '@ant-design/icons'; import { XProvider } from '@ant-design/x'; -import { Tooltip, Button } from 'antd'; +import { Button, Tooltip } from 'antd'; import classNames from 'classnames'; import { DumiDemoGrid, FormattedMessage } from 'dumi'; +import React, { Suspense, useContext } from 'react'; import useLayoutState from '../../../hooks/useLayoutState'; import useLocale from '../../../hooks/useLocale'; @@ -106,7 +106,9 @@ const DemoWrapper: typeof DumiDemoGrid = ({ items }) => { - + + + ); diff --git a/.dumi/theme/builtins/LocaleLink/index.tsx b/.dumi/theme/builtins/LocaleLink/index.tsx index 3086392d..692e2539 100644 --- a/.dumi/theme/builtins/LocaleLink/index.tsx +++ b/.dumi/theme/builtins/LocaleLink/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { Link } from 'dumi'; import useLocale from '../../../hooks/useLocale'; +import Link from '../../../theme/common/Link'; type LinkProps = Parameters[0]; diff --git a/.dumi/theme/common/Link.tsx b/.dumi/theme/common/Link.tsx index 7d84c205..dd855110 100644 --- a/.dumi/theme/common/Link.tsx +++ b/.dumi/theme/common/Link.tsx @@ -1,7 +1,6 @@ -import { Link as DumiLink, useLocation, useNavigate } from 'dumi'; -import nprogress from 'nprogress'; +import { Link as DumiLink, useAppData, useLocation, useNavigate } from 'dumi'; import type { MouseEvent, MouseEventHandler } from 'react'; -import React, { forwardRef, useLayoutEffect, useTransition } from 'react'; +import React, { useMemo, forwardRef } from 'react'; export interface LinkProps { to: string | { pathname?: string; search?: string; hash?: string }; @@ -9,64 +8,49 @@ export interface LinkProps { className?: string; onClick?: MouseEventHandler; component?: React.ComponentType; + children?: React.ReactNode; } -nprogress.configure({ showSpinner: false }); - -const Link = forwardRef>((props, ref) => { - const { to, children, component, ...rest } = props; - const [isPending, startTransition] = useTransition(); - const navigate = useNavigate(); - const { pathname } = useLocation(); - - const href = React.useMemo(() => { - if (typeof to === 'object') { - return `${to.pathname || pathname}${to.search || ''}${to.hash || ''}`; - } - return to; - }, [to]); - - const handleClick = (e: MouseEvent) => { - props.onClick?.(e); - if (!href?.startsWith('http')) { - // Should support open in new tab - if (!e.metaKey && !e.ctrlKey && !e.shiftKey) { - e.preventDefault(); - startTransition(() => { - if (href) { - navigate(href); - } - }); +const Link = forwardRef>( + ({ component, children, to, ...rest }, ref) => { + const { pathname } = useLocation(); + const { preloadRoute } = useAppData(); + const navigate = useNavigate(); + const href = useMemo(() => { + if (typeof to === 'object') { + return `${to.pathname || pathname}${to.search || ''}${to.hash || ''}`; } + return to; + }, [to]); + const onClick = (e: MouseEvent) => { + rest.onClick?.(e); + if (!href?.startsWith('http')) { + // Should support open in new tab + if (!e.metaKey && !e.ctrlKey && !e.shiftKey) { + e.preventDefault(); + navigate(href); + } + } + }; + if (component) { + return React.createElement( + component, + { + ...rest, + ref, + href, + onClick, + onMouseEnter: () => preloadRoute?.(href), + }, + children, + ); } - }; - - useLayoutEffect(() => { - if (isPending) { - nprogress.start(); - } else { - nprogress.done(); - } - }, [isPending]); - - if (component) { - return React.createElement( - component, - { - ...rest, - ref, - onClick: handleClick, - href, - }, - children, + return ( + + {children} + ); - } - - return ( - - {children} - - ); -}); + }, +); export default Link; diff --git a/.dumi/theme/slots/Footer/index.tsx b/.dumi/theme/slots/Footer/index.tsx index fef42b1c..7d6c3418 100644 --- a/.dumi/theme/slots/Footer/index.tsx +++ b/.dumi/theme/slots/Footer/index.tsx @@ -15,13 +15,14 @@ import { import { TinyColor } from '@ctrl/tinycolor'; import { createStyles } from 'antd-style'; import getAlphaColor from 'antd/es/theme/util/getAlphaColor'; -import { FormattedMessage, Link } from 'dumi'; +import { FormattedMessage } from 'dumi'; import RcFooter from 'rc-footer'; import type { FooterColumn } from 'rc-footer/lib/column'; import React, { useContext } from 'react'; import useLocale from '../../../hooks/useLocale'; import useLocation from '../../../hooks/useLocation'; +import Link from '../../../theme/common/Link'; import SiteContext from '../SiteContext'; import AdditionalInfo from './AdditionalInfo'; diff --git a/.dumirc.ts b/.dumirc.ts index 5c2e244f..a3cdd5b8 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -8,7 +8,11 @@ import { version } from './package.json'; export default defineConfig({ plugins: ['dumi-plugin-color-chunk'], + + // For + routePrefetch: {}, manifest: {}, + conventionRoutes: { // to avoid generate routes for .dumi/pages/index/components/xx exclude: [/index\/components\//], diff --git a/bun.lockb b/bun.lockb index 96a35dd4..0707bedf 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/bubble/Bubble.tsx b/components/bubble/Bubble.tsx index 1ef00ed6..8591bb5f 100644 --- a/components/bubble/Bubble.tsx +++ b/components/bubble/Bubble.tsx @@ -4,6 +4,8 @@ import React from 'react'; import { Avatar } from 'antd'; import useXComponentConfig from '../_util/hooks/use-x-component-config'; import { useXProviderContext } from '../x-provider'; +import Editor from './Editor'; +import useMergedConfig from './hooks/useMergedConfig'; import useTypedEffect from './hooks/useTypedEffect'; import useTypingConfig from './hooks/useTypingConfig'; import type { BubbleProps } from './interface'; @@ -40,6 +42,7 @@ const Bubble: React.ForwardRefRenderFunction = (props, r onTypingComplete, header, footer, + editable = {}, ...otherHtmlProps } = props; @@ -60,6 +63,28 @@ const Bubble: React.ForwardRefRenderFunction = (props, r // ===================== Component Config ========================= const contextConfig = useXComponentConfig('bubble'); + // =========================== Editable =========================== + const [enableEdit, editConfig] = useMergedConfig(editable); + const [isEditing, setIsEditing] = React.useState(editConfig?.editing || false); + + React.useEffect(() => { + setIsEditing(editConfig?.editing || false); + }, [editConfig?.editing]); + + const onEditChange = (value: string) => { + editConfig?.onChange?.(value); + }; + + const onEditCancel = () => { + editConfig?.onCancel?.(); + setIsEditing(false); + }; + + const onEditEnd = (value: string) => { + editConfig?.onEnd?.(value); + setIsEditing(false); + }; + // ============================ Typing ============================ const [typingEnabled, typingStep, typingInterval] = useTypingConfig(typing); @@ -119,23 +144,43 @@ const Bubble: React.ForwardRefRenderFunction = (props, r contentNode = mergedContent as React.ReactNode; } - let fullContent: React.ReactNode = ( -
- {contentNode} -
- ); + let fullContent: React.ReactNode = + enableEdit && isEditing ? ( + + ) : ( +
+ {contentNode} +
+ ); if (header || footer) { fullContent = ( diff --git a/components/bubble/Editor.tsx b/components/bubble/Editor.tsx new file mode 100644 index 00000000..39a000f8 --- /dev/null +++ b/components/bubble/Editor.tsx @@ -0,0 +1,120 @@ +import classNames from 'classnames'; +import * as React from 'react'; + +import { Button, Flex, Input } from 'antd'; +import { TextAreaRef } from 'antd/lib/input/TextArea'; +import { EditConfig } from './interface'; +import useStyle from './style'; + +const { TextArea } = Input; + +interface EditableProps { + prefixCls: string; + value: string; + onChange?: (value: string) => void; + onCancel?: () => void; + onEnd?: (value: string) => void; + editorClassName?: string; + editorStyle?: React.CSSProperties; + editorTextAreaConfig?: EditConfig['editorTextAreaConfig']; + editorButtonConfig?: EditConfig['editorButtonConfig']; +} + +const Editor: React.FC = (props) => { + const { + prefixCls, + editorClassName: className, + editorStyle, + value, + onChange, + onCancel, + onEnd, + editorTextAreaConfig, + editorButtonConfig, + } = props; + const textAreaRef = React.useRef(null); + + const [current, setCurrent] = React.useState(value); + + React.useEffect(() => { + setCurrent(value); + }, [value]); + + React.useEffect(() => { + if (textAreaRef.current?.resizableTextArea) { + const { textArea } = textAreaRef.current.resizableTextArea; + textArea.focus(); + const { length } = textArea.value; + textArea.setSelectionRange(length, length); + } + }, []); + + const onTextAreaChange: React.ChangeEventHandler = ({ target }) => { + setCurrent(target.value.replace(/[\n\r]/g, '')); + onChange?.(target.value.replace(/[\n\r]/g, '')); + }; + + const confirmEnd = () => { + onEnd?.(current.trim()); + }; + + const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); + + const editorClassName = classNames( + prefixCls, + `${prefixCls}-editor`, + className, + hashId, + cssVarCls, + ); + + const CancelButton = () => + editorButtonConfig ? ( + editorButtonConfig + .filter((config) => config.type === 'cancel') + .map((config, index) => ( + + )) + ) : ( + + ); + const SaveButton = () => + editorButtonConfig ? ( + editorButtonConfig + .filter((config) => config.type === 'save') + .map((config, index) => ( + + )) + ) : ( + + ); + + return wrapCSSVar( +
+ +