Compare commits

..

5 Commits

Author SHA1 Message Date
高艳兵
6e993d2ef8 fix: correct mismatched JSDoc parameter names in plugin files (#56969) 2026-02-12 20:54:15 +08:00
luozz
a46cd9d8ba fix: TreeSelect's multi-line text will cause CheckBox compression. #56957 (#56961)
Co-authored-by: 罗忠泽 <victor.luo@spotterio.com>
2026-02-12 14:59:49 +08:00
Clayton
75819fc3a7 fix(Typography): prevent ellipsis tooltip when hovering copy button (#56855)
* fix(Typography): prevent ellipsis tooltip when hovering copy button

* fix: add ellipsis debug

* fix: test

* fix: test

* fix: test

* fix: skip editable

* fix: test

* fix: test input

* update snap

* update input snap

* fix: add mouse leave event ellipsis test

* fix: add detailed testing

* fix: remove skip

* fix: update input snap

* Revert "fix: update input snap"

This reverts commit 36ededd5c8be4ab86bd261cd41c9cef89251c9d9.

* Reapply "fix: update input snap"

This reverts commit 353b74d2de.

* fix: fix: update input snap

* remove typography skip test

* update snap

* update typography demo snap

* chore: mv to style file

* fix: fix onMouseEnter/onMouseLeave not working with ellipsis/tooltip

Extract onMouseEnter/onMouseLeave from props and use them directly in event handlers to fix the issue where these events don't work properly when Typography has ellipsis enabled with tooltip configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: simplify EllipsisTooltip open logic

Use nullish coalescing operator to simplify open calculation.
Change showEllipsisTooltip from optional to required.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: further simplify EllipsisTooltip prop names and logic

Rename showEllipsisTooltip to open for consistency with Tooltip API.
Simplify open calculation to use direct boolean logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update snap

* update snap

* test: update snapshot

---------

Co-authored-by: thinkasany <480968828@qq.com>
Co-authored-by: 二货机器人 <smith3816@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 12:02:55 +08:00
renovate[bot]
a900301eac chore(deps): update dependency @types/react to v19.2.14 (#56955)
* chore(deps): update dependency @types/react to v19.2.14

* fix

* fix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: thinkasany <480968828@qq.com>
2026-02-12 10:52:42 +08:00
renovate[bot]
06c9af5e98 chore(deps): update actions-cool/issues-helper digest to e361abf (#56954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: thinkasany <480968828@qq.com>
2026-02-12 09:34:41 +08:00
24 changed files with 2218 additions and 2251 deletions

View File

@@ -46,8 +46,6 @@ export interface GroupProps {
decoration?: React.ReactNode;
/** 预加载的背景图片列表 */
backgroundPrefetchList?: string[];
/** 标题右侧的操作按钮 */
extra?: React.ReactNode;
}
const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
@@ -61,7 +59,6 @@ const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
background,
collapse,
backgroundPrefetchList,
extra,
} = props;
// 预加载背景图片
@@ -90,29 +87,18 @@ const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
<div className={styles.container}>{decoration}</div>
<GroupMaskLayer style={{ paddingBlock: token.marginFarSM }}>
<div className={styles.typographyWrapper}>
<div
<Typography.Title
id={id}
level={1}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: token.paddingXS,
fontWeight: 900,
color: titleColor,
// Special for the title
fontSize: isMobile ? token.fontSizeHeading2 : token.fontSizeHeading1,
}}
>
<Typography.Title
id={id}
level={1}
style={{
fontWeight: 900,
color: titleColor,
margin: 0,
// Special for the title
fontSize: isMobile ? token.fontSizeHeading2 : token.fontSizeHeading1,
}}
>
{title}
</Typography.Title>
{extra}
</div>
{title}
</Typography.Title>
<Typography.Paragraph
style={{
color: titleColor,

View File

@@ -4,16 +4,15 @@ import {
Alert,
App,
Button,
Card,
Checkbox,
ColorPicker,
ConfigProvider,
DatePicker,
Dropdown,
Flex,
Modal,
Progress,
Radio,
Segmented,
Select,
Slider,
Space,
@@ -27,7 +26,6 @@ import clsx from 'clsx';
import useLocale from '../../../../hooks/useLocale';
const { _InternalPanelDoNotUseOrYouWillBeFired: ModalPanel } = Modal;
const { Group: RadioButtonGroup, Button: RadioButton } = Radio;
const locales = {
cn: {
@@ -50,9 +48,6 @@ const locales = {
icon: '图标按钮',
hello: '你好Ant Design!',
release: 'Ant Design 6.0 正式发布!',
segmentedDaily: '每日',
segmentedWeekly: '每周',
segmentedMonthly: '每月',
},
en: {
range: 'Set Range',
@@ -74,9 +69,6 @@ const locales = {
icon: 'Icon',
hello: 'Hello, Ant Design!',
release: 'Ant Design 6.0 is released!',
segmentedDaily: 'Daily',
segmentedWeekly: 'Weekly',
segmentedMonthly: 'Monthly',
},
};
@@ -103,7 +95,7 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
return (
<ConfigProvider {...config}>
<div className={clsx(containerClassName, styles.container)}>
<Card className={clsx(containerClassName, styles.container)}>
<App>
<Flex vertical gap="middle" style={style} className={className}>
<ModalPanel title="Ant Design" width="100%">
@@ -128,32 +120,13 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
</Space.Compact>
</div>
<ColorPicker showText defaultValue="#1677ff" style={{ flex: 'none' }} />
<ColorPicker style={{ flex: 'none' }} />
<Select
style={{ flex: 'auto' }}
mode="multiple"
maxTagCount="responsive"
defaultValue={['apple', 'banana']}
options={[
{ value: 'apple', label: locale.apple },
{ value: 'banana', label: locale.banana },
{ value: 'orange', label: locale.orange },
{ value: 'watermelon', label: locale.watermelon },
]}
/>
</Flex>
{/* Filled variants */}
<Flex gap="middle">
<DatePicker variant="filled" />
<Select
variant="filled"
style={{ flex: 'auto' }}
mode="multiple"
maxTagCount="responsive"
defaultValue={['apple', 'banana']}
defaultValue={[{ value: 'apple' }, { value: 'banana' }]}
options={[
{ value: 'apple', label: locale.apple },
{ value: 'banana', label: locale.banana },
@@ -174,7 +147,20 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
]}
/>
{/* Line */}
<Slider defaultValue={50} />
<Slider
style={{ marginInline: 20 }}
range
marks={{
0: '0°C',
26: '26°C',
37: '37°C',
100: {
style: { color: '#f50' },
label: <strong>100°C</strong>,
},
}}
defaultValue={[26, 37]}
/>
{/* Line */}
<Flex gap="middle">
<Button type="primary" className={styles.flexAuto}>
@@ -202,20 +188,9 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
/>
<Radio.Group defaultValue={locale.apple} options={[locale.apple, locale.banana]} />
</Flex>
<Flex gap="middle" align="center">
<RadioButtonGroup defaultValue="a">
<RadioButton value="a">A</RadioButton>
<RadioButton value="b">B</RadioButton>
<RadioButton value="c">C</RadioButton>
</RadioButtonGroup>
<Segmented
defaultValue={locale.segmentedDaily}
options={[locale.segmentedDaily, locale.segmentedWeekly, locale.segmentedMonthly]}
/>
</Flex>
</Flex>
</App>
</div>
</Card>
</ConfigProvider>
);
};

View File

@@ -13,15 +13,11 @@ const locales = {
cn: {
themeTitle: '定制主题,随心所欲',
themeDesc: '开放样式算法与语义化结构,让你与 AI 一起轻松定制主题',
aiGenerate: 'AI 生成主题',
aiGenerateDesc: '用一句话描述你想要的风格',
},
en: {
themeTitle: 'Flexible theme customization',
themeDesc:
'Open style algorithms and semantic structures make it easy for you and AI to customize themes',
aiGenerate: 'AI Generate Theme',
aiGenerateDesc: 'Describe your desired style',
},
};
@@ -44,7 +40,7 @@ const useStyles = createStyles(({ css, cssVar }) => ({
listStyleType: 'none',
display: 'flex',
flexDirection: 'column',
gap: cssVar.paddingSM,
gap: cssVar.paddingMD,
}),
listItem: css({
margin: 0,
@@ -57,7 +53,7 @@ const useStyles = createStyles(({ css, cssVar }) => ({
borderColor: 'transparent',
transition: `all ${cssVar.motionDurationMid} ${cssVar.motionEaseInOut}`,
'&:hover:not(.active):not(.ai-generate-item)': {
'&:hover:not(.active)': {
borderColor: cssVar.colorPrimaryBorder,
backgroundColor: cssVar.colorPrimaryBg,
cursor: 'pointer',
@@ -86,39 +82,6 @@ const useStyles = createStyles(({ css, cssVar }) => ({
},
}),
// AI Generate Item
aiGenerateItem: css({
borderStyle: 'dashed',
opacity: 0.7,
cursor: 'pointer',
color: cssVar.colorTextSecondary,
paddingInline: cssVar.padding,
'&:hover': {
borderColor: cssVar.colorPrimary,
color: cssVar.colorPrimary,
opacity: 1,
},
}),
aiGenerateContent: css({
position: 'relative',
zIndex: 1,
}),
aiGenerateIcon: css({
fontSize: 14,
marginRight: 6,
opacity: 0.6,
}),
aiGenerateDesc: css({
fontSize: cssVar.fontSizeSM,
opacity: 0.5,
marginTop: 2,
fontWeight: 400,
}),
// Components
componentsBlockContainer: css({
flex: 'auto',
@@ -135,12 +98,7 @@ const useStyles = createStyles(({ css, cssVar }) => ({
}),
}));
export interface ThemePreviewProps {
onOpenPromptDrawer?: () => void;
}
export default function ThemePreview(props: ThemePreviewProps = {}) {
const { onOpenPromptDrawer } = props;
export default function ThemePreview() {
const [locale] = useLocale(locales);
const { styles } = useStyles();
const isDark = React.use(DarkContext);
@@ -153,7 +111,10 @@ export default function ThemePreview(props: ThemePreviewProps = {}) {
const defaultThemeName = isDark ? 'dark' : 'light';
const targetTheme =
previewThemes.find((theme) => theme.key === defaultThemeName)?.name || previewThemes[0].name;
process.env.NODE_ENV !== 'production'
? previewThemes[previewThemes.length - 1].name
: previewThemes.find((theme) => theme.key === defaultThemeName)?.name ||
previewThemes[0].name;
setActiveName(targetTheme);
}, [isDark]);
@@ -187,50 +148,24 @@ export default function ThemePreview(props: ThemePreviewProps = {}) {
backgroundPrefetchList={backgroundPrefetchList}
>
<Flex className={styles.container} gap="large">
<div
style={{
display: 'flex',
}}
>
<div className={styles.list} role="tablist" aria-label="Theme selection">
{previewThemes.map((theme) => (
<div
className={clsx(
styles.listItem,
activeName === theme.name && 'active',
activeTheme?.bgImgDark && 'dark',
)}
key={theme.name}
role="tab"
tabIndex={activeName === theme.name ? 0 : -1}
aria-selected={activeName === theme.name}
onClick={() => handleThemeClick(theme.name)}
onKeyDown={(event) => handleKeyDown(event, theme.name)}
style={{ marginBottom: 8 }}
>
{theme.name}
</div>
))}
{/* AI 生成主题 - 最后一个选项 */}
<div className={styles.list} role="tablist" aria-label="Theme selection">
{previewThemes.map((theme) => (
<div
className={clsx(styles.listItem, styles.aiGenerateItem, 'ai-generate-item')}
className={clsx(
styles.listItem,
activeName === theme.name && 'active',
activeTheme?.bgImgDark && 'dark',
)}
key={theme.name}
role="tab"
tabIndex={0}
onClick={onOpenPromptDrawer}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onOpenPromptDrawer?.();
}
}}
tabIndex={activeName === theme.name ? 0 : -1}
aria-selected={activeName === theme.name}
onClick={() => handleThemeClick(theme.name)}
onKeyDown={(event) => handleKeyDown(event, theme.name)}
>
<div className={styles.aiGenerateContent}>
<span className={styles.aiGenerateIcon}>🎨</span>
<span>{locale.aiGenerate}</span>
</div>
<div className={styles.aiGenerateDesc}>{locale.aiGenerateDesc}</div>
{theme.name}
</div>
</div>
))}
</div>
<ComponentsBlock
key={activeName}

View File

@@ -8,6 +8,7 @@ import type { UseTheme } from '.';
const useStyles = createStyles(({ css, cssVar }) => {
const glassBorder = {
border: `${cssVar.lineWidth} solid rgba(255,255,255,0.3)`,
boxShadow: [
`${cssVar.boxShadowSecondary}`,
`inset 0 0 5px 2px rgba(255, 255, 255, 0.3)`,

View File

@@ -1,4 +1,4 @@
import React, { Suspense, useState } from 'react';
import React, { Suspense } from 'react';
import { theme } from 'antd';
import { createStaticStyles } from 'antd-style';
@@ -8,9 +8,6 @@ import BannerRecommends from './components/BannerRecommends';
import Group from './components/Group';
import PreviewBanner from './components/PreviewBanner';
import ThemePreview from './components/ThemePreview';
import PromptDrawer from '../../theme/common/ThemeSwitch/PromptDrawer';
import SiteContext from '../../theme/slots/SiteContext';
import type { SiteContextProps } from '../../theme/slots/SiteContext';
const ComponentsList = React.lazy(() => import('./components/ComponentsList'));
const DesignFramework = React.lazy(() => import('./components/DesignFramework'));
@@ -45,16 +42,6 @@ const Homepage: React.FC = () => {
const { token } = theme.useToken();
const isDark = React.use(DarkContext);
const [promptDrawerOpen, setPromptDrawerOpen] = useState(false);
const siteContext = React.use(SiteContext);
const handlePromptDrawerOpen = () => setPromptDrawerOpen(true);
const handlePromptDrawerClose = () => setPromptDrawerOpen(false);
const handleThemeChange = (themeConfig: SiteContextProps['dynamicTheme']) => {
if (siteContext?.updateSiteConfig) {
siteContext.updateSiteConfig({ dynamicTheme: themeConfig });
}
};
return (
<section>
@@ -62,14 +49,13 @@ const Homepage: React.FC = () => {
<BannerRecommends />
</PreviewBanner>
<ThemePreview onOpenPromptDrawer={handlePromptDrawerOpen} />
{/* AI 生成主题抽屉 */}
<PromptDrawer
open={promptDrawerOpen}
onClose={handlePromptDrawerClose}
onThemeChange={handleThemeChange}
/>
{/* 定制主题 */}
{/* <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
<Suspense fallback={null}>
<Theme />
</Suspense>
</ConfigProvider> */}
<ThemePreview />
{/* 组件列表 */}
<Group
@@ -104,6 +90,55 @@ const Homepage: React.FC = () => {
</Group>
</section>
);
// return (
// <section>
// <PreviewBanner>
// <BannerRecommends />
// </PreviewBanner>
// <div>
// {/* 定制主题 */}
// <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
// <Suspense fallback={null}>
// <Theme />
// </Suspense>
// </ConfigProvider>
// {/* 组件列表 */}
// <Group
// background={token.colorBgElevated}
// collapse
// title={locale.assetsTitle}
// description={locale.assetsDesc}
// id="design"
// >
// <Suspense fallback={null}>
// <ComponentsList />
// </Suspense>
// </Group>
// {/* 设计语言 */}
// <Group
// title={locale.designTitle}
// description={locale.designDesc}
// background={isDark ? '#393F4A' : '#F5F8FF'}
// decoration={
// <img
// draggable={false}
// className={classNames.image}
// src="https://gw.alipayobjects.com/zos/bmw-prod/ba37a413-28e6-4be4-b1c5-01be1a0ebb1c.svg"
// alt="bg"
// />
// }
// >
// <Suspense fallback={null}>
// <DesignFramework />
// </Suspense>
// </Group>
// </div>
// </section>
// );
};
export default Homepage;

View File

@@ -1,46 +1,22 @@
import React, { useRef, useState } from 'react';
import { UserOutlined } from '@ant-design/icons';
import { Bubble, Prompts, Sender, Welcome } from '@ant-design/x';
import type { BubbleItemType } from '@ant-design/x/es/bubble/interface';
import type { PromptsItemType } from '@ant-design/x';
import { AntDesignOutlined, UserOutlined } from '@ant-design/icons';
import { Bubble, Sender } from '@ant-design/x';
import type { SenderRef } from '@ant-design/x/es/sender';
import { Button, Divider, Drawer, Flex, Skeleton, Splitter, Typography } from 'antd';
import { Drawer, Flex, Typography } from 'antd';
import type { GetProp } from 'antd';
import type { SiteContextProps } from '../../../theme/slots/SiteContext';
import SiteContext from '../../../theme/slots/SiteContext';
import useLocale from '../../../hooks/useLocale';
import ComponentsBlock from '../../../pages/index/components/ThemePreview/ComponentsBlock';
import type { SiteContextProps } from '../../../theme/slots/SiteContext';
import usePromptTheme from './usePromptTheme';
import usePromptRecommend from './usePromptRecommend';
const antdLogoSrc = 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg';
const THEME_EMOJIS = ['🌅', '🌊', '🌿', '🍂', '🌸', '🌌', '🎨', '⚡', '🔮', '🪐'];
const getEmojiForTheme = (index: number) => THEME_EMOJIS[index % THEME_EMOJIS.length];
const locales = {
cn: {
title: '🎨 AI 生成主题',
finishTips: '生成主题完成,已应用',
placeholder: '描述你想要的主题风格,如:温暖阳光、清新自然、科技感...',
welcomeTitle: 'AI 主题生成器',
welcomeDescription: '描述你想要的风格,我会为你生成专属主题',
recommendTitle: '推荐主题',
loading: '加载中...',
refresh: '换一换',
resetToDefault: '恢复默认主题',
title: 'AI 生成主题',
finishTips: '生成完成,对话以重新生成。',
},
en: {
title: '🎨 AI Theme Generator',
finishTips: 'Theme generated and applied',
placeholder: 'Describe your desired theme style, e.g., warm sunny, fresh natural, tech feel...',
welcomeTitle: 'AI Theme Generator',
welcomeDescription: 'Describe your desired style and I will generate a custom theme for you',
recommendTitle: 'Recommended Themes',
loading: 'Loading...',
refresh: 'Refresh',
resetToDefault: 'Reset to default theme',
title: 'AI Theme Generator',
finishTips: 'Completed. Regenerate by start a new conversation.',
},
};
@@ -50,40 +26,17 @@ export interface PromptDrawerProps {
onThemeChange?: (themeConfig: SiteContextProps['dynamicTheme']) => void;
}
// Extended type for Prompts items with additional properties
interface ExtendedPromptsItemType extends PromptsItemType {
originalDescription?: string;
isRefresh?: boolean;
}
const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChange }) => {
const { updateSiteConfig, isDark } = React.use(SiteContext) as SiteContextProps;
const [locale, localeKey] = useLocale(locales);
const [locale] = useLocale(locales);
const [inputValue, setInputValue] = useState('');
const senderRef = useRef<SenderRef>(null);
const [submitPrompt, loading, prompt, resText, cancelRequest] = usePromptTheme(onThemeChange);
const {
recommendations,
loading: recommendLoading,
fetch: fetchRecommendations,
} = usePromptRecommend(localeKey);
const handleSubmit = React.useCallback(
(value: string) => {
submitPrompt(value);
setInputValue('');
},
[submitPrompt],
);
const handleRefreshRecommendations = React.useCallback(() => {
fetchRecommendations(`prompt-drawer-refresh-${Date.now()}`);
}, [fetchRecommendations]);
const handleResetToDefaultTheme = () => {
updateSiteConfig({ dynamicTheme: undefined });
const handleSubmit = (value: string) => {
submitPrompt(value);
setInputValue('');
};
const handleAfterOpenChange = (isOpen: boolean) => {
@@ -91,18 +44,14 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
// Focus the Sender component when drawer opens
senderRef.current.focus?.();
}
// Fetch AI recommendations when drawer opens
if (isOpen) {
fetchRecommendations('prompt-drawer-init');
}
};
const items = React.useMemo<BubbleItemType[]>(() => {
const items = React.useMemo<GetProp<typeof Bubble.List, 'items'>>(() => {
if (!prompt) {
return [];
}
const nextItems: BubbleItemType[] = [
const nextItems: GetProp<typeof Bubble.List, 'items'> = [
{
key: 1,
role: 'user',
@@ -113,274 +62,54 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
},
{
key: 2,
role: 'ai',
role: 'system',
placement: 'start',
content: resText,
avatar: <img src={antdLogoSrc} alt="Ant Design" style={{ width: 28, height: 28 }} />,
avatar: <AntDesignOutlined />,
loading: !resText,
contentRender: (content: string) => (
<Typography>
<pre
style={{
margin: 0,
padding: '16px',
borderRadius: 8,
background: isDark
? 'linear-gradient(135deg, rgba(90,196,255,0.08) 0%, rgba(174,136,255,0.08) 100%)'
: 'linear-gradient(135deg, #f2f9fe 0%, #f7f3ff 100%)',
fontSize: 13,
lineHeight: 1.6,
border: 'none',
color: isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)',
}}
>
{content}
</pre>
<pre style={{ margin: 0 }}>{content}</pre>
</Typography>
),
styles: {
content: {
background: 'transparent',
padding: 0,
border: 'none',
},
},
},
];
if (!loading) {
nextItems.push({
key: 3,
role: 'system',
role: 'divider',
placement: 'start',
shape: 'round',
content: locale.finishTips,
});
// Add recommended themes prompts
const recommendedPrompts: ExtendedPromptsItemType[] = recommendations
.slice(0, 4)
.map((text, index) => ({
key: `rec-${text}`,
description: `${getEmojiForTheme(index)} ${text}`,
originalDescription: text,
}));
// Add refresh button
recommendedPrompts.push({
key: 'refresh',
description: `🔄 ${locale.refresh}`,
isRefresh: true,
});
nextItems.push({
key: 4,
role: 'ai',
placement: 'start',
content: '',
avatar: <img src={antdLogoSrc} alt="Ant Design" style={{ width: 28, height: 28 }} />,
contentRender: () =>
recommendLoading ? (
<Flex gap={8} wrap style={{ justifyContent: 'center' }}>
{Array.from({ length: 4 }).map((_, index) => (
<Skeleton.Input
key={index}
active
size="small"
style={{ width: 140, borderRadius: 8 }}
/>
))}
</Flex>
) : (
<Prompts
wrap
items={recommendedPrompts}
onItemClick={({ data }) => {
if ('isRefresh' in data && data.isRefresh) {
handleRefreshRecommendations();
} else {
handleSubmit(
String(
(data as ExtendedPromptsItemType).originalDescription ?? data.description,
),
);
}
}}
styles={{ root: { marginTop: 8 } }}
/>
),
styles: { content: { padding: 0, background: 'transparent' } },
avatar: <AntDesignOutlined />,
shape: 'corner',
});
}
return nextItems;
}, [
prompt,
resText,
loading,
recommendLoading,
locale.finishTips,
isDark,
recommendations,
handleSubmit,
handleRefreshRecommendations,
locale.refresh,
]);
// Limit to 3 recommendations for Prompts component + refresh button
const prompts: ExtendedPromptsItemType[] = React.useMemo(() => {
const themePrompts: ExtendedPromptsItemType[] = recommendations
.slice(0, 3)
.map((text, index) => ({
key: text,
description: `${getEmojiForTheme(index)} ${text}`,
originalDescription: text,
}));
// Add refresh button as last item only when we have recommendations
if (themePrompts.length > 0) {
themePrompts.push({
key: 'refresh',
description: `🔄 ${locale.refresh}`,
isRefresh: true,
});
}
return themePrompts;
}, [recommendations, locale.refresh]);
const renderedWelcome = React.useMemo(
() => (
<div style={{ padding: '0 0 16px' }}>
<Welcome
icon={<img src={antdLogoSrc} alt="Ant Design" style={{ width: 48, height: 48 }} />}
title={locale.welcomeTitle}
description={locale.welcomeDescription}
styles={{
root: {
background: isDark
? 'linear-gradient(97deg, rgba(90,196,255,0.12) 0%, rgba(174,136,255,0.12) 100%)'
: 'linear-gradient(97deg, #f2f9fe 0%, #f7f3ff 100%)',
},
}}
/>
</div>
),
[locale.welcomeTitle, locale.welcomeDescription, isDark],
);
const renderedPrompts = React.useMemo(
() => (
<div style={{ padding: '0 0 32px' }}>
<Flex vertical gap={12} align="center">
<Divider titlePlacement="center" style={{ margin: 0, fontSize: 12 }}>
{locale.recommendTitle}
</Divider>
{recommendLoading ? (
<Flex gap={8} wrap style={{ justifyContent: 'center' }}>
{Array.from({ length: 3 }).map((_, index) => (
<Skeleton.Input
key={index}
active
size="small"
style={{ width: 140, borderRadius: 8 }}
/>
))}
</Flex>
) : (
prompts.length > 0 && (
<Prompts
wrap
items={prompts}
onItemClick={({ data }) => {
if ('isRefresh' in data && data.isRefresh) {
handleRefreshRecommendations();
} else {
handleSubmit(
String(
(data as ExtendedPromptsItemType).originalDescription ?? data.description,
),
);
}
}}
styles={{
root: {
marginTop: 4,
},
item: {
borderRadius: 8,
},
}}
/>
)
)}
</Flex>
</div>
),
[locale.recommendTitle, recommendLoading, prompts, handleSubmit, handleRefreshRecommendations],
);
}, [prompt, resText, loading, locale.finishTips]);
return (
<Drawer
title={locale.title}
open={open}
onClose={onClose}
width="80vw"
size={480}
placement="right"
afterOpenChange={handleAfterOpenChange}
extra={
<Button type="text" size="small" onClick={handleResetToDefaultTheme}>
{locale.resetToDefault}
</Button>
}
>
<Splitter style={{ height: '100%' }}>
{/* 左侧预览区域 */}
<Splitter.Panel defaultSize="50%" min="30%" max="70%">
<Flex vertical style={{ height: '100%', padding: '0 8px' }}>
<div
style={{
flex: 1,
padding: '16px 8px',
overflow: 'auto',
}}
>
<ComponentsBlock className="prompt-drawer-preview" />
</div>
</Flex>
</Splitter.Panel>
{/* 右侧对话区域 */}
<Splitter.Panel defaultSize="50%" min="30%" max="70%">
<Flex vertical gap={0} style={{ height: '100%', padding: '0 8px' }}>
<div
style={{
flex: 1,
padding: 0,
overflow: 'auto',
}}
>
{!prompt ? (
<>
{renderedWelcome}
{renderedPrompts}
</>
) : (
<Bubble.List items={items} />
)}
</div>
<Sender
ref={senderRef}
value={inputValue}
onChange={setInputValue}
onSubmit={handleSubmit}
loading={loading}
onCancel={cancelRequest}
placeholder={locale.placeholder}
/>
</Flex>
</Splitter.Panel>
</Splitter>
<Flex vertical style={{ height: '100%' }}>
<Bubble.List style={{ flex: 1, overflow: 'auto' }} items={items} />
<Sender
ref={senderRef}
style={{ flex: 0 }}
value={inputValue}
onChange={setInputValue}
onSubmit={handleSubmit}
loading={loading}
onCancel={cancelRequest}
/>
</Flex>
</Drawer>
);
};

View File

@@ -1,158 +0,0 @@
import { XStream } from '@ant-design/x-sdk';
import { useRef, useState } from 'react';
const locales = {
cn: {
recommendPrompt:
'请生成 4 个 Tailwindcss 主题描述词,需要多样化的 UI 设计风格,用于 Ant Design 主题生成器推荐。参考官方内建风格:暗黑风格、类 MUI 风格、类 shadcn 风格、卡通风格、插画风格、类 Bootstrap 拟物化风格、玻璃风格、极客风格。回复格式:用逗号分隔。',
},
en: {
recommendPrompt:
'Generate 4 Tailwind CSS theme names featuring diverse UI design styles for an Ant Design theme generator recommendation. Reference the official built-in styles: Dark, MUI-like, shadcn-like, Cartoon, Illustration, Bootstrap-like Skeuomorphic, Glassmorphism, and Geek. Output format: comma-separated.',
},
};
const FALLBACK_THEMES = {
cn: [
'温暖阳光的橙色调,营造活力积极的氛围',
'专业稳重的深海蓝商务风格',
'清新自然的森林绿环保主题',
'极客紫霓虹感的科技前沿风格',
'柔和粉紫的樱花春日浪漫主题',
'高对比度的赛博朋克深色科技风',
'莫兰迪灰色调,简约优雅的现代感',
'青花瓷蓝白配色,东方雅韵',
'马卡龙多彩配色,活泼童趣',
'水墨黑白灰,传统韵味',
],
en: [
'Warm sunny orange tones for energetic positive vibes',
'Professional deep ocean blue business style',
'Fresh natural forest green eco-friendly theme',
'Geek purple neon tech cutting-edge style',
'Soft pink-purple cherry blossom spring romantic theme',
'High contrast cyberpunk dark tech style',
'Morandi gray tones, minimalist elegant modern feel',
'Blue and white porcelain colors, Eastern elegance',
'Colorful macaron, lively and playful',
'Ink black white gray, traditional charm',
],
};
const fetchRecommendations = async (
localeKey: keyof typeof locales,
abortSignal?: AbortSignal,
): Promise<string[]> => {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: locales[localeKey].recommendPrompt,
userId: 'AntDesignSite',
}),
signal: abortSignal,
};
try {
const response = await fetch('https://api.x.ant.design/api/agent_tbox_antd', options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!response.body) {
throw new Error('Response body is null');
}
let fullContent = '';
for await (const chunk of XStream({
readableStream: response.body,
})) {
if (chunk.event === 'message') {
try {
const data = JSON.parse(chunk.data) as {
lane: string;
payload: string;
};
const payload = JSON.parse(data.payload) as {
text: string;
};
fullContent += payload.text || '';
} catch {
// Skip malformed chunks and continue
}
}
}
// Parse theme names from response - separated by commas
const text = fullContent.trim();
const items = text
.split(/[,\n]/)
.map((item) => item.trim())
.filter(Boolean);
const result = items.slice(0, 4);
// If parsing failed or got no results, use fallback
if (result.length === 0) {
return FALLBACK_THEMES[localeKey];
}
return result;
} catch (error) {
// Don't log AbortError - it's expected when cancelling requests
if (error instanceof Error && error.name === 'AbortError') {
throw error; // Re-throw AbortError to be handled by caller
}
console.error('Error in fetchRecommendations:', error);
// Return fallback themes
return FALLBACK_THEMES[localeKey];
}
};
export default function usePromptRecommend(localeKey: keyof typeof locales = 'cn') {
const [recommendations, setRecommendations] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const fetchedKeyRef = useRef<string>('');
const fetch = async (key: string) => {
if (fetchedKeyRef.current === key) {
return;
}
// Cancel previous request if it exists
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
setLoading(true);
try {
const items = await fetchRecommendations(localeKey, abortController.signal);
setRecommendations(items);
fetchedKeyRef.current = key;
} catch (error) {
if (!(error instanceof Error && error.name === 'AbortError')) {
console.error('Failed to fetch recommendations:', error);
// Use fallback on error
setRecommendations(FALLBACK_THEMES[localeKey]);
fetchedKeyRef.current = key;
}
} finally {
// Only clear loading and controller if this is still the active request
if (abortControllerRef.current === abortController) {
setLoading(false);
abortControllerRef.current = null;
}
}
};
return { recommendations, loading, fetch };
}

View File

@@ -255,7 +255,7 @@ function antdCodeAppend(docFileAbs: string, src: string): string {
*
* @param md - 原始 markdown 内容
* @param docFileAbs - 文档文件的绝对路径,用于解析相对路径和检测语言
* @param enablePickLocaleBlock - 是否启用多语言块提取,可以是布尔值或函数,默认为 true
* @param codeAppend - 代码追加函数:在替换 <code src> 标签时,用于追加额外的内容(如 demo 描述信息)
* @returns 替换后的 markdown 内容
*/
function replaceCodeSrcToMarkdown(
@@ -556,7 +556,6 @@ function emitRawMd(api: IApi) {
* 2. 在 HTML 文件导出阶段输出处理后的 raw markdown 文件
*
* @param api - Dumi API 实例
* @param options - 插件配置选项
*/
export default function rawMdPlugin(api: IApi) {
// 注册配置键,允许用户在配置中使用 rawMd 键

View File

@@ -19,8 +19,8 @@ function extractSemantics(objContent: string): Record<string, string> {
}
/**
* 从 _semantic*.tsx 文件中提取语义信息
* @param semanticFile - _semantic*.tsx 文件的绝对路径
* 从 _semantic*.tsx 文件内容中提取语义信息
* @param content - _semantic*.tsx 文件的文件内容字符串
* @returns 包含中文和英文语义描述的对象,失败返回 null
*/
function extractLocaleInfoFromContent(content: string): {

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: (github.event.pull_request.head.ref == 'next' || github.event.pull_request.head.ref == 'feature' || github.event.pull_request.head.ref == 'master') && github.event.pull_request.head.user.login == 'ant-design'
steps:
- uses: actions-cool/issues-helper@e2ff99831a4f13625d35064e2b3dfe65c07a0396
- uses: actions-cool/issues-helper@e361abf610221f09495ad510cb1e69328d839e1c
with:
actions: create-comment
issue-number: ${{ github.event.number }}

View File

@@ -110,10 +110,12 @@ const ColorTrigger = forwardRef<HTMLDivElement, ColorTriggerProps>((props, ref)
prefixCls={prefixCls}
color={color.toCssString()}
className={classNames.body}
innerClassName={classNames.content}
style={styles.body}
innerStyle={styles.content}
/>
),
[color, prefixCls, classNames.body, styles.body],
[color, prefixCls, classNames.body, classNames.content, styles.body, styles.content],
);
return (

View File

@@ -27,8 +27,8 @@ const OTPInput = React.forwardRef<InputRef, OTPInputProps>((props, ref) => {
React.useImperativeHandle(ref, () => inputRef.current!);
// ========================= Input ==========================
const onInternalChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
onChange(index, e.target.value);
const onInternalChange: React.InputEventHandler<HTMLInputElement> = (e) => {
onChange(index, (e.target as HTMLInputElement).value);
};
// ========================= Focus ==========================

View File

@@ -680,52 +680,56 @@ Array [
class="ant-typography css-var-test-id"
>
Ant Design
<button
aria-describedby="test-id"
aria-label="Copy"
class="ant-typography-copy"
type="button"
<span
class="ant-typography-actions"
>
<span
aria-label="copy"
class="anticon anticon-copy"
role="img"
>
<svg
aria-hidden="true"
data-icon="copy"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
/>
</svg>
</span>
</button>
<div
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast ant-tooltip-css-var css-var-test-id ant-tooltip-placement-top"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; right: auto; bottom: auto; box-sizing: border-box;"
>
<div
class="ant-tooltip-arrow"
style="position: absolute; bottom: 0px; left: 0px;"
<button
aria-describedby="test-id"
aria-label="Copy"
class="ant-typography-copy"
type="button"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
aria-label="copy"
class="anticon anticon-copy"
role="img"
>
<svg
aria-hidden="true"
data-icon="copy"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
/>
</svg>
</span>
</button>
<div
class="ant-tooltip-container"
id="test-id"
role="tooltip"
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast ant-tooltip-css-var css-var-test-id ant-tooltip-placement-top"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; right: auto; bottom: auto; box-sizing: border-box;"
>
Copy
<div
class="ant-tooltip-arrow"
style="position: absolute; bottom: 0px; left: 0px;"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-container"
id="test-id"
role="tooltip"
>
Copy
</div>
</div>
</div>
</span>
</span>,
<span
class="ant-input-affix-wrapper ant-input-outlined css-var-test-id ant-input-css-var"

View File

@@ -408,31 +408,35 @@ Array [
class="ant-typography css-var-test-id"
>
Ant Design
<button
aria-label="Copy"
class="ant-typography-copy"
type="button"
<span
class="ant-typography-actions"
>
<span
aria-label="copy"
class="anticon anticon-copy"
role="img"
<button
aria-label="Copy"
class="ant-typography-copy"
type="button"
>
<svg
aria-hidden="true"
data-icon="copy"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="copy"
class="anticon anticon-copy"
role="img"
>
<path
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
/>
</svg>
</span>
</button>
<svg
aria-hidden="true"
data-icon="copy"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
/>
</svg>
</span>
</button>
</span>
</span>,
<span
class="ant-input-affix-wrapper ant-input-outlined css-var-test-id ant-input-css-var"

View File

@@ -280,6 +280,12 @@ export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject =>
.equal(),
},
// >>> Checkbox
// https://github.com/ant-design/ant-design/issues/56957
[`${treeCls}-checkbox`]: {
flexShrink: 0,
},
// >>> Switcher
[`${treeCls}-switcher`]: {
...getSwitchStyle(prefixCls, token),

View File

@@ -7,12 +7,15 @@ export interface EllipsisTooltipProps {
tooltipProps?: TooltipProps;
enableEllipsis: boolean;
isEllipsis?: boolean;
/** When true, show the ellipsis tooltip; when false, hide it. Fully controlled so tooltip re-opens when moving from copy button back to text. */
open: boolean;
children: React.ReactElement;
}
const EllipsisTooltip: React.FC<EllipsisTooltipProps> = ({
enableEllipsis,
isEllipsis,
open,
children,
tooltipProps,
}) => {
@@ -20,8 +23,9 @@ const EllipsisTooltip: React.FC<EllipsisTooltipProps> = ({
return children;
}
const mergedOpen = open && isEllipsis;
return (
<Tooltip open={isEllipsis ? undefined : false} {...tooltipProps}>
<Tooltip open={mergedOpen} {...tooltipProps}>
{children}
</Tooltip>
);

View File

@@ -65,8 +65,9 @@ export interface EllipsisConfig {
tooltip?: React.ReactNode | TooltipProps;
}
export interface BlockProps<C extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements>
extends TypographyProps<C> {
export interface BlockProps<
C extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements,
> extends TypographyProps<C> {
title?: string;
editable?: boolean | EditConfig;
copyable?: boolean | CopyConfig;
@@ -133,6 +134,8 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
copyable,
component,
title,
onMouseEnter,
onMouseLeave,
...restProps
} = props;
const { getPrefixCls, direction } = React.useContext(ConfigContext);
@@ -262,6 +265,8 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
};
const [ellipsisWidth, setEllipsisWidth] = React.useState(0);
const [isHoveringOperations, setIsHoveringOperations] = React.useState(false);
const [isHoveringTypography, setIsHoveringTypography] = React.useState(false);
const onResize = ({ offsetWidth }: { offsetWidth: number }) => {
setEllipsisWidth(offsetWidth);
};
@@ -407,11 +412,28 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
);
};
const renderOperations = (canEllipsis: boolean) => [
canEllipsis && renderExpand(),
renderEdit(),
renderCopy(),
];
const renderOperations = (canEllipsis: boolean) => {
const expandNode = canEllipsis && renderExpand();
const editNode = renderEdit();
const copyNode = renderCopy();
if (!expandNode && !editNode && !copyNode) {
return null;
}
return (
<span
key="operations"
className={`${prefixCls}-actions`}
onMouseEnter={() => setIsHoveringOperations(true)}
onMouseLeave={() => setIsHoveringOperations(false)}
>
{expandNode}
{editNode}
{copyNode}
</span>
);
};
const renderEllipsis = (canEllipsis: boolean) => [
canEllipsis && !expanded && (
@@ -430,8 +452,17 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
tooltipProps={tooltipProps}
enableEllipsis={mergedEnableEllipsis}
isEllipsis={isMergedEllipsis}
open={isHoveringTypography && !isHoveringOperations}
>
<Typography
onMouseEnter={(e) => {
setIsHoveringTypography(true);
onMouseEnter?.(e);
}}
onMouseLeave={(e) => {
setIsHoveringTypography(false);
onMouseLeave?.(e);
}}
className={clsx(
{
[`${prefixCls}-${type}`]: type,

View File

@@ -685,4 +685,51 @@ describe('Typography.Ellipsis', () => {
expect(expandButtonCN).toHaveTextContent('展开');
expect(expandButtonCN).toBeInTheDocument();
});
it('copyable + ellipsis: ellipsis tooltip hides when hovering copy, shows when hovering text', async () => {
offsetWidth = 50;
scrollWidth = 100;
const ref = React.createRef<HTMLElement>();
const { container, baseElement } = render(
<Base ref={ref} component="p" copyable ellipsis={{ rows: 1, tooltip: true }}>
{fullStr}
</Base>,
);
triggerResize(ref.current!);
await waitFakeTimer();
const copyBtn = container.querySelector('.ant-typography-copy');
const operationsWrapper = copyBtn?.parentElement;
expect(operationsWrapper).toBeTruthy();
const typographyEl = ref.current!;
const getTooltipContent = () =>
baseElement.querySelector('[role="tooltip"]')?.textContent?.trim();
fireEvent.mouseEnter(typographyEl);
await waitFakeTimer();
await waitFor(() => {
expect(getTooltipContent()).toContain(fullStr);
});
fireEvent.mouseEnter(operationsWrapper!);
await waitFakeTimer();
await waitFor(() => {
const ellipsisTooltip = baseElement.querySelector('[role="tooltip"]');
expect(ellipsisTooltip?.closest('.ant-tooltip')).toHaveClass('ant-tooltip-hidden');
});
fireEvent.mouseLeave(operationsWrapper!);
fireEvent.mouseEnter(typographyEl);
await waitFakeTimer();
await waitFor(() => {
expect(getTooltipContent()).toContain(fullStr);
});
fireEvent.mouseLeave(typographyEl);
fireEvent.mouseLeave(operationsWrapper!);
});
});

View File

@@ -1,7 +1,7 @@
## zh-CN
多行文本省略。
多行文本省略。页面底部包含「可复制 + 省略」时 tooltip 行为的调试区块,便于验证:悬停文字显示省略 tooltip悬停复制按钮仅显示复制 tooltip从复制按钮移回文字时省略 tooltip 再次出现。
## en-US
Multiple line ellipsis support.
Multiple line ellipsis support. The bottom section is a debug block for copyable + ellipsis tooltip behavior: hover text for ellipsis tooltip, hover copy button for copy-only tooltip, then move back to text to confirm the ellipsis tooltip shows again.

View File

@@ -108,6 +108,22 @@ const App: React.FC = () => {
<Text style={{ width: 100, whiteSpace: 'nowrap' }} ellipsis copyable>
{templateStr}
</Text>
<div style={{ marginTop: 24 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
<strong>Debug: copyable + ellipsis tooltips</strong>
<br />
1. Hover the text ellipsis tooltip (full content) should show.
<br />
2. Hover the copy button only &quot;Copy&quot; / &quot;Copied&quot; tooltip should show.
<br />
3. Move from copy button back to the text (without leaving the block) ellipsis tooltip
should show again.
</div>
<Text style={{ width: 280, display: 'block' }} ellipsis={{ tooltip: true }} copyable>
{templateStr}
</Text>
</div>
</>
);
};

View File

@@ -107,6 +107,10 @@ const genTypographyStyle: GenerateStyle<TypographyToken> = (token) => {
...getLinkStyles(token),
// Operation
[`${componentCls}-actions`]: {
display: 'inline',
},
[`
${componentCls}-expand,
${componentCls}-collapse,

View File

@@ -210,7 +210,7 @@
"@types/pngjs": "^6.0.5",
"@types/prismjs": "^1.26.5",
"@types/progress": "^2.0.7",
"@types/react": "19.2.9",
"@types/react": "19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-highlight-words": "^0.20.0",
"@types/semver": "^7.7.1",