mirror of
https://github.com/ant-design/ant-design.git
synced 2026-02-15 22:09:21 +08:00
Compare commits
25 Commits
docs/theme
...
typography
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a4cc2cbd3 | ||
|
|
e790b66a1e | ||
|
|
a052a5d2be | ||
|
|
3314988e39 | ||
|
|
36b916d8e0 | ||
|
|
1d4c4dd188 | ||
|
|
a2d889fb17 | ||
|
|
1c8eb5f00b | ||
|
|
a97fa60d90 | ||
|
|
5d2020f5cb | ||
|
|
a192155765 | ||
|
|
1edaa53a76 | ||
|
|
38f32d5cf7 | ||
|
|
065ff3d2ad | ||
|
|
77fecc07ca | ||
|
|
75819fc3a7 | ||
|
|
08a0e26973 | ||
|
|
0088e0221c | ||
|
|
a900301eac | ||
|
|
06c9af5e98 | ||
|
|
88bd8f6de8 | ||
|
|
5d3c93bdd6 | ||
|
|
61729477bb | ||
|
|
fdb0d6ca6e | ||
|
|
e93868198b |
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
2
.github/workflows/pr-check-merge.yml
vendored
2
.github/workflows/pr-check-merge.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface AppProps<P = AnyObject> extends AppConfig {
|
||||
component?: CustomComponent<P> | false;
|
||||
}
|
||||
|
||||
const App: React.FC<AppProps> = (props) => {
|
||||
const App = React.forwardRef<HTMLElement, AppProps>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
children,
|
||||
@@ -79,6 +79,12 @@ const App: React.FC<AppProps> = (props) => {
|
||||
'When using cssVar, ensure `component` is assigned a valid React component string.',
|
||||
);
|
||||
|
||||
devUseWarning('App')(
|
||||
!ref || component !== false,
|
||||
'usage',
|
||||
'`ref` is not supported when `component` is `false`. Please provide a valid `component` instead.',
|
||||
);
|
||||
|
||||
// ============================ Render ============================
|
||||
const Component = component === false ? React.Fragment : component;
|
||||
|
||||
@@ -90,7 +96,7 @@ const App: React.FC<AppProps> = (props) => {
|
||||
return (
|
||||
<AppContext.Provider value={memoizedContextValue}>
|
||||
<AppConfigContext.Provider value={mergedAppConfig}>
|
||||
<Component {...(component === false ? undefined : rootProps)}>
|
||||
<Component {...(component === false ? undefined : { ...rootProps, ref })}>
|
||||
{ModalContextHolder}
|
||||
{messageContextHolder}
|
||||
{notificationContextHolder}
|
||||
@@ -99,7 +105,7 @@ const App: React.FC<AppProps> = (props) => {
|
||||
</AppConfigContext.Provider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
App.displayName = 'App';
|
||||
|
||||
@@ -247,5 +247,20 @@ describe('App', () => {
|
||||
'Warning: [antd: App] When using cssVar, ensure `component` is assigned a valid React component string.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn if component is false and ref is not empty', () => {
|
||||
const domRef = React.createRef<HTMLSpanElement>();
|
||||
render(<App ref={domRef} component={false} />);
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: App] `ref` is not supported when `component` is `false`. Please provide a valid `component` instead.',
|
||||
);
|
||||
});
|
||||
|
||||
it('App should support Ref', () => {
|
||||
const domRef = React.createRef<HTMLSpanElement>();
|
||||
const { container } = render(<App ref={domRef} className="bamboo" component="span" />);
|
||||
expect(domRef.current).toBe(container.querySelector('.bamboo'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
|
||||
import ConfigProvider from '..';
|
||||
import { Button, InputNumber, Select } from '../..';
|
||||
import { Button, InputNumber, Select, Space } from '../..';
|
||||
import { render, waitFakeTimer } from '../../../tests/utils';
|
||||
import theme from '../../theme';
|
||||
import type { GlobalToken } from '../../theme/internal';
|
||||
@@ -113,6 +113,21 @@ describe('ConfigProvider.Theme', () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support Addon component token', () => {
|
||||
const { container } = render(
|
||||
<ConfigProvider theme={{ components: { Addon: { colorText: '#0000FF', algorithm: true } } }}>
|
||||
<Space.Compact>
|
||||
<Space.Addon className="test-addon">Addon Content</Space.Addon>
|
||||
</Space.Compact>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
|
||||
const addon = container.querySelector('.test-addon')!;
|
||||
expect(addon).toHaveStyle({
|
||||
'--ant-color-text': '#0000FF',
|
||||
});
|
||||
});
|
||||
|
||||
it('hashed should be true if not changed', () => {
|
||||
let hashId = 'hashId';
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ import type { TourProps } from '../tour/interface';
|
||||
import type { TransferProps } from '../transfer';
|
||||
import type { TreeProps } from '../tree';
|
||||
import type { TreeSelectProps } from '../tree-select';
|
||||
import type { TypographyClassNamesType, TypographyStylesType } from '../typography/Base';
|
||||
import type { UploadProps } from '../upload';
|
||||
import type { RenderEmptyHandler } from './defaultRenderEmpty';
|
||||
|
||||
@@ -242,6 +243,11 @@ export type AlertConfig = ComponentStyleConfig &
|
||||
|
||||
export type BadgeConfig = ComponentStyleConfig & Pick<BadgeProps, 'classNames' | 'styles'>;
|
||||
|
||||
export type TypographyConfig = ComponentStyleConfig & {
|
||||
classNames?: TypographyClassNamesType;
|
||||
styles?: TypographyStylesType;
|
||||
};
|
||||
|
||||
export type BreadcrumbConfig = ComponentStyleConfig &
|
||||
Pick<BreadcrumbProps, 'classNames' | 'styles' | 'separator' | 'dropdownIcon'>;
|
||||
|
||||
@@ -447,7 +453,7 @@ export interface ConfigComponentProps {
|
||||
collapse?: CollapseConfig;
|
||||
floatButton?: FloatButtonConfig;
|
||||
floatButtonGroup?: FloatButtonGroupConfig;
|
||||
typography?: ComponentStyleConfig;
|
||||
typography?: TypographyConfig;
|
||||
skeleton?: SkeletonConfig;
|
||||
spin?: SpinConfig;
|
||||
segmented?: SegmentedConfig;
|
||||
|
||||
@@ -74,6 +74,7 @@ import type {
|
||||
TourConfig,
|
||||
TransferConfig,
|
||||
TreeSelectConfig,
|
||||
TypographyConfig,
|
||||
UploadConfig,
|
||||
Variant,
|
||||
WaveConfig,
|
||||
@@ -225,7 +226,7 @@ export interface ConfigProviderProps {
|
||||
collapse?: CollapseConfig;
|
||||
divider?: ComponentStyleConfig;
|
||||
drawer?: DrawerConfig;
|
||||
typography?: ComponentStyleConfig;
|
||||
typography?: TypographyConfig;
|
||||
skeleton?: SkeletonConfig;
|
||||
spin?: SpinConfig;
|
||||
segmented?: ComponentStyleConfig;
|
||||
|
||||
@@ -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 ==========================
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -121,10 +121,6 @@ const genSingleStyle: GenerateStyle<SelectToken> = (token) => {
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
[`&-active:not(${selectItemCls}-option-disabled)`]: {
|
||||
backgroundColor: token.optionActiveBg,
|
||||
},
|
||||
|
||||
[`&-selected:not(${selectItemCls}-option-disabled)`]: {
|
||||
color: token.optionSelectedColor,
|
||||
fontWeight: token.optionSelectedFontWeight,
|
||||
@@ -135,6 +131,10 @@ const genSingleStyle: GenerateStyle<SelectToken> = (token) => {
|
||||
},
|
||||
},
|
||||
|
||||
[`&-active:not(${selectItemCls}-option-disabled)`]: {
|
||||
backgroundColor: token.optionActiveBg,
|
||||
},
|
||||
|
||||
'&-disabled': {
|
||||
[`&${selectItemCls}-option-selected`]: {
|
||||
backgroundColor: token.colorBgContainerDisabled,
|
||||
|
||||
@@ -16066,6 +16066,28 @@ exports[`renders components/space/demo/compact-nested.tsx extend context correct
|
||||
|
||||
exports[`renders components/space/demo/compact-nested.tsx extend context correctly 2`] = `[]`;
|
||||
|
||||
exports[`renders components/space/demo/component-token.tsx extend context correctly 1`] = `
|
||||
<div
|
||||
class="ant-space-compact"
|
||||
>
|
||||
<div
|
||||
class="ant-space-addon ant-space-addon-compact-item ant-space-addon-compact-first-item css-var-test-id ant-space-addon-variant-outlined"
|
||||
>
|
||||
Addon
|
||||
</div>
|
||||
<button
|
||||
class="ant-btn css-var-test-id ant-btn-primary ant-btn-color-primary ant-btn-variant-solid ant-btn-compact-item ant-btn-compact-last-item"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Button
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/space/demo/component-token.tsx extend context correctly 2`] = `[]`;
|
||||
|
||||
exports[`renders components/space/demo/debug.tsx extend context correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small css-var-test-id"
|
||||
|
||||
@@ -4097,6 +4097,26 @@ exports[`renders components/space/demo/compact-nested.tsx correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/space/demo/component-token.tsx correctly 1`] = `
|
||||
<div
|
||||
class="ant-space-compact"
|
||||
>
|
||||
<div
|
||||
class="ant-space-addon ant-space-addon-compact-item ant-space-addon-compact-first-item css-var-test-id ant-space-addon-variant-outlined"
|
||||
>
|
||||
Addon
|
||||
</div>
|
||||
<button
|
||||
class="ant-btn css-var-test-id ant-btn-primary ant-btn-color-primary ant-btn-variant-solid ant-btn-compact-item ant-btn-compact-last-item"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Button
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/space/demo/debug.tsx correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small css-var-test-id"
|
||||
|
||||
7
components/space/demo/component-token.md
Normal file
7
components/space/demo/component-token.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
使用 `ConfigProvider` 自定义 `Space.Addon` 的主题样式。
|
||||
|
||||
## en-US
|
||||
|
||||
Use `ConfigProvider` to customize the theme of `Space.Addon`.
|
||||
19
components/space/demo/component-token.tsx
Normal file
19
components/space/demo/component-token.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Button, ConfigProvider, Space } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Addon: { colorText: 'blue', algorithm: true },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Space.Compact>
|
||||
<Space.Addon>Addon</Space.Addon>
|
||||
<Button type="primary">Button</Button>
|
||||
</Space.Compact>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
export default App;
|
||||
@@ -34,6 +34,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*37T2R6O9oi0AAA
|
||||
<code src="./demo/debug.tsx" debug>Diverse Child</code>
|
||||
<code src="./demo/gap-in-line.tsx" debug>Flex gap style</code>
|
||||
<code src="./demo/style-class.tsx" version="6.0.0">Custom semantic dom styling</code>
|
||||
<code src="./demo/component-token.tsx" debug>Customize Addon with theme</code>
|
||||
|
||||
## API
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*37T2R6O9oi0AAA
|
||||
<code src="./demo/debug.tsx" debug>多样的 Child</code>
|
||||
<code src="./demo/gap-in-line.tsx" debug>Flex gap 样式</code>
|
||||
<code src="./demo/style-class.tsx" version="6.0.0">自定义语义结构的样式和类</code>
|
||||
<code src="./demo/component-token.tsx" debug>自定义主题</code>
|
||||
|
||||
## API
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resetComponent } from '../../style';
|
||||
import { genCompactItemStyle } from '../../style/compact-item';
|
||||
import { genStyleHooks } from '../../theme/internal';
|
||||
import type { FullToken, GenerateStyle } from '../../theme/internal';
|
||||
@@ -7,11 +8,11 @@ import { genCssVar } from '../../theme/util/genStyleUtils';
|
||||
// biome-ignore lint/suspicious/noEmptyInterface: ComponentToken need to be empty by default
|
||||
export interface ComponentToken {}
|
||||
|
||||
interface SpaceToken extends FullToken<'Space'> {
|
||||
interface AddonToken extends FullToken<'Space'> {
|
||||
// Custom token here
|
||||
}
|
||||
|
||||
const genSpaceAddonStyle: GenerateStyle<SpaceToken> = (token) => {
|
||||
const genSpaceAddonStyle: GenerateStyle<AddonToken> = (token) => {
|
||||
const {
|
||||
componentCls,
|
||||
borderRadius,
|
||||
@@ -27,7 +28,7 @@ const genSpaceAddonStyle: GenerateStyle<SpaceToken> = (token) => {
|
||||
antCls,
|
||||
} = token;
|
||||
|
||||
const [varName, varRef] = genCssVar(antCls, 'space');
|
||||
const [varName, varRef] = genCssVar(antCls, 'space-addon');
|
||||
|
||||
return {
|
||||
[componentCls]: [
|
||||
@@ -35,6 +36,7 @@ const genSpaceAddonStyle: GenerateStyle<SpaceToken> = (token) => {
|
||||
// == Base ==
|
||||
// ==========================================================
|
||||
{
|
||||
...resetComponent(token),
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0,
|
||||
@@ -144,7 +146,7 @@ const genSpaceAddonStyle: GenerateStyle<SpaceToken> = (token) => {
|
||||
};
|
||||
|
||||
// ============================== Export ==============================
|
||||
export default genStyleHooks(['Space', 'Addon'], (token) => [
|
||||
export default genStyleHooks('Addon', (token) => [
|
||||
genSpaceAddonStyle(token),
|
||||
genCompactItemStyle(token, { focus: false }),
|
||||
]);
|
||||
|
||||
@@ -49,6 +49,7 @@ import type { ComponentToken as SelectComponentToken } from '../../select/style'
|
||||
import type { ComponentToken as SkeletonComponentToken } from '../../skeleton/style';
|
||||
import type { ComponentToken as SliderComponentToken } from '../../slider/style';
|
||||
import type { ComponentToken as SpaceComponentToken } from '../../space/style';
|
||||
import type { ComponentToken as AddonComponentToken } from '../../space/style/addon';
|
||||
import type { ComponentToken as SpinComponentToken } from '../../spin/style';
|
||||
import type { ComponentToken as SplitterComponentToken } from '../../splitter/style';
|
||||
import type { ComponentToken as StatisticComponentToken } from '../../statistic/style';
|
||||
@@ -68,6 +69,7 @@ import type { ComponentToken as UploadComponentToken } from '../../upload/style'
|
||||
|
||||
export interface ComponentTokenMap {
|
||||
Affix?: AffixComponentToken;
|
||||
Addon?: AddonComponentToken;
|
||||
Alert?: AlertComponentToken;
|
||||
Anchor?: AnchorComponentToken;
|
||||
Avatar?: AvatarComponentToken;
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface CopyBtnProps extends Omit<CopyConfig, 'onCopy'> {
|
||||
onCopy: React.MouseEventHandler<HTMLButtonElement>;
|
||||
iconOnly: boolean;
|
||||
loading: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const CopyBtn: React.FC<CopyBtnProps> = ({
|
||||
@@ -28,6 +30,8 @@ const CopyBtn: React.FC<CopyBtnProps> = ({
|
||||
tabIndex,
|
||||
onCopy,
|
||||
loading: btnLoading,
|
||||
className,
|
||||
style,
|
||||
}) => {
|
||||
const tooltipNodes = toList(tooltips);
|
||||
const iconNodes = toList(icon);
|
||||
@@ -40,10 +44,11 @@ const CopyBtn: React.FC<CopyBtnProps> = ({
|
||||
<Tooltip title={copyTitle}>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(`${prefixCls}-copy`, {
|
||||
className={clsx(`${prefixCls}-copy`, className, {
|
||||
[`${prefixCls}-copy-success`]: copied,
|
||||
[`${prefixCls}-copy-icon-only`]: iconOnly,
|
||||
})}
|
||||
style={style}
|
||||
onClick={onCopy}
|
||||
aria-label={ariaLabel}
|
||||
tabIndex={tabIndex}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -8,9 +8,10 @@ import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect';
|
||||
import { composeRef } from '@rc-component/util/lib/ref';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import type { SemanticType } from '../../_util/hooks';
|
||||
import isNonNullable from '../../_util/isNonNullable';
|
||||
import { isStyleSupport } from '../../_util/styleChecker';
|
||||
import { ConfigContext } from '../../config-provider';
|
||||
import type { DirectionType } from '../../config-provider';
|
||||
import useLocale from '../../locale/useLocale';
|
||||
import type { TooltipProps } from '../../tooltip';
|
||||
import Tooltip from '../../tooltip';
|
||||
@@ -19,6 +20,7 @@ import useCopyClick from '../hooks/useCopyClick';
|
||||
import useMergedConfig from '../hooks/useMergedConfig';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
import useTooltipProps from '../hooks/useTooltipProps';
|
||||
import { useTypographySemantic } from '../hooks/useTypographySemantic';
|
||||
import type { TypographyProps } from '../Typography';
|
||||
import Typography from '../Typography';
|
||||
import CopyBtn from './CopyBtn';
|
||||
@@ -28,6 +30,41 @@ import { isEleEllipsis, isValidText } from './util';
|
||||
|
||||
export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';
|
||||
|
||||
// Base typography props without generic parameter for semantic types
|
||||
export interface BaseTypographyProps extends React.HTMLAttributes<HTMLElement> {
|
||||
id?: string;
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
rootClassName?: string;
|
||||
style?: React.CSSProperties;
|
||||
classNames?: TypographyClassNamesType;
|
||||
styles?: TypographyStylesType;
|
||||
children?: React.ReactNode;
|
||||
'aria-label'?: string;
|
||||
direction?: DirectionType;
|
||||
/** @private */
|
||||
component?: keyof JSX.IntrinsicElements;
|
||||
}
|
||||
|
||||
export type TypographySemanticClassNames = {
|
||||
root?: string;
|
||||
actions?: string;
|
||||
action?: string;
|
||||
};
|
||||
|
||||
export type TypographySemanticStyles = {
|
||||
root?: React.CSSProperties;
|
||||
actions?: React.CSSProperties;
|
||||
action?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export type TypographyClassNamesType = SemanticType<
|
||||
BaseTypographyProps,
|
||||
TypographySemanticClassNames
|
||||
>;
|
||||
|
||||
export type TypographyStylesType = SemanticType<BaseTypographyProps, TypographySemanticStyles>;
|
||||
|
||||
export interface CopyConfig {
|
||||
text?: string | (() => string | Promise<string>);
|
||||
onCopy?: (event?: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
@@ -125,6 +162,9 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
style,
|
||||
classNames,
|
||||
styles,
|
||||
direction: typographyDirection,
|
||||
type,
|
||||
disabled,
|
||||
children,
|
||||
@@ -133,16 +173,22 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
copyable,
|
||||
component,
|
||||
title,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
...restProps
|
||||
} = props;
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const [textLocale] = useLocale('Text');
|
||||
|
||||
const typographyRef = React.useRef<HTMLElement>(null);
|
||||
const editIconRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
// ============================ MISC ============================
|
||||
const prefixCls = getPrefixCls('typography', customizePrefixCls);
|
||||
const [mergedClassNames, mergedStyles, prefixCls, direction] = useTypographySemantic(
|
||||
customizePrefixCls,
|
||||
classNames,
|
||||
styles,
|
||||
typographyDirection,
|
||||
props,
|
||||
);
|
||||
|
||||
const textProps = omit(restProps, DECORATION_PROPS);
|
||||
|
||||
@@ -262,6 +308,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);
|
||||
};
|
||||
@@ -351,7 +399,11 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
<button
|
||||
type="button"
|
||||
key="expand"
|
||||
className={`${prefixCls}-${expanded ? 'collapse' : 'expand'}`}
|
||||
className={clsx(
|
||||
`${prefixCls}-${expanded ? 'collapse' : 'expand'}`,
|
||||
mergedClassNames.action,
|
||||
)}
|
||||
style={mergedStyles.action}
|
||||
onClick={(e) => onExpandClick(e!, { expanded: !expanded })}
|
||||
aria-label={expanded ? textLocale.collapse : textLocale?.expand}
|
||||
>
|
||||
@@ -376,7 +428,8 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
<button
|
||||
type="button"
|
||||
ref={editIconRef}
|
||||
className={`${prefixCls}-edit`}
|
||||
className={clsx(`${prefixCls}-edit`, mergedClassNames.action)}
|
||||
style={mergedStyles.action}
|
||||
onClick={onEditClick}
|
||||
aria-label={ariaLabel}
|
||||
tabIndex={tabIndex}
|
||||
@@ -403,15 +456,35 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
onCopy={onCopyClick}
|
||||
loading={copyLoading}
|
||||
iconOnly={!isNonNullable(children)}
|
||||
className={mergedClassNames.action}
|
||||
style={mergedStyles.action}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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={clsx(`${prefixCls}-actions`, mergedClassNames.actions)}
|
||||
style={mergedStyles.actions}
|
||||
onMouseEnter={() => setIsHoveringOperations(true)}
|
||||
onMouseLeave={() => setIsHoveringOperations(false)}
|
||||
>
|
||||
{expandNode}
|
||||
{editNode}
|
||||
{copyNode}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEllipsis = (canEllipsis: boolean) => [
|
||||
canEllipsis && !expanded && (
|
||||
@@ -430,8 +503,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,
|
||||
@@ -443,6 +525,8 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
},
|
||||
className,
|
||||
)}
|
||||
classNames={classNames}
|
||||
styles={styles}
|
||||
prefixCls={customizePrefixCls}
|
||||
style={{
|
||||
...style,
|
||||
|
||||
@@ -3,77 +3,120 @@ import type { JSX } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import type { DirectionType } from '../config-provider';
|
||||
import { useComponentConfig } from '../config-provider/context';
|
||||
import type {
|
||||
BaseTypographyProps,
|
||||
TypographySemanticClassNames,
|
||||
TypographySemanticStyles,
|
||||
} from './Base';
|
||||
import { useTypographySemantic } from './hooks/useTypographySemantic';
|
||||
import useStyle from './style';
|
||||
|
||||
export interface TypographyProps<C extends keyof JSX.IntrinsicElements>
|
||||
extends React.HTMLAttributes<HTMLElement> {
|
||||
id?: string;
|
||||
prefixCls?: string;
|
||||
extends BaseTypographyProps {
|
||||
/** @internal */
|
||||
component?: C;
|
||||
}
|
||||
|
||||
interface InternalProps {
|
||||
className?: string;
|
||||
rootClassName?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
/** @internal */
|
||||
component?: C;
|
||||
'aria-label'?: string;
|
||||
component?: keyof JSX.IntrinsicElements;
|
||||
direction?: DirectionType;
|
||||
classNames?: TypographySemanticClassNames;
|
||||
styles?: TypographySemanticStyles;
|
||||
prefixCls: string;
|
||||
}
|
||||
|
||||
interface InternalTypographyProps<C extends keyof JSX.IntrinsicElements>
|
||||
extends TypographyProps<C> {}
|
||||
|
||||
const Typography = React.forwardRef<
|
||||
HTMLElement,
|
||||
InternalTypographyProps<keyof JSX.IntrinsicElements>
|
||||
>((props, ref) => {
|
||||
const InternalTypography = React.forwardRef<HTMLElement, InternalProps>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
component: Component = 'article',
|
||||
className,
|
||||
rootClassName,
|
||||
children,
|
||||
direction: typographyDirection,
|
||||
direction,
|
||||
style,
|
||||
classNames,
|
||||
styles,
|
||||
prefixCls,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
getPrefixCls,
|
||||
direction: contextDirection,
|
||||
className: contextClassName,
|
||||
style: contextStyle,
|
||||
} = useComponentConfig('typography');
|
||||
|
||||
const direction = typographyDirection ?? contextDirection;
|
||||
const prefixCls = getPrefixCls('typography', customizePrefixCls);
|
||||
|
||||
// Style
|
||||
const [hashId, cssVarCls] = useStyle(prefixCls);
|
||||
|
||||
const componentClassName = clsx(
|
||||
prefixCls,
|
||||
contextClassName,
|
||||
hashId,
|
||||
cssVarCls,
|
||||
{
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
},
|
||||
className,
|
||||
rootClassName,
|
||||
hashId,
|
||||
cssVarCls,
|
||||
classNames?.root,
|
||||
);
|
||||
|
||||
const mergedStyle: React.CSSProperties = { ...contextStyle, ...style };
|
||||
const mergedStyle: React.CSSProperties = {
|
||||
...styles?.root,
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
// @ts-expect-error: Expression produces a union type that is too complex to represent.
|
||||
<Component className={componentClassName} style={mergedStyle} ref={ref} {...restProps}>
|
||||
<Component {...restProps} className={componentClassName} style={mergedStyle} ref={ref}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
InternalTypography.displayName = 'InternalTypography';
|
||||
}
|
||||
|
||||
const Typography = React.forwardRef<HTMLElement, TypographyProps<keyof JSX.IntrinsicElements>>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
rootClassName,
|
||||
children,
|
||||
direction: typographyDirection,
|
||||
style,
|
||||
classNames,
|
||||
styles,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const [mergedClassNames, mergedStyles, prefixCls, direction] = useTypographySemantic(
|
||||
customizePrefixCls,
|
||||
classNames,
|
||||
styles,
|
||||
typographyDirection,
|
||||
props,
|
||||
);
|
||||
|
||||
return (
|
||||
<InternalTypography
|
||||
ref={ref}
|
||||
component="article"
|
||||
className={clsx(className, rootClassName)}
|
||||
direction={direction}
|
||||
style={style}
|
||||
classNames={mergedClassNames}
|
||||
styles={mergedStyles}
|
||||
prefixCls={prefixCls}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</InternalTypography>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Typography.displayName = 'Typography';
|
||||
}
|
||||
|
||||
export default Typography;
|
||||
export { InternalTypography };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,379 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`renders components/typography/demo/_semantic.tsx correctly 1`] = `
|
||||
<div
|
||||
class="acss-1ucck97"
|
||||
>
|
||||
<div
|
||||
class="ant-row css-var-test-id"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-col-16 acss-my3sst css-var-test-id"
|
||||
>
|
||||
<div
|
||||
aria-label="Ant Design is a design language for background applications, refined by Ant UED Team. It aims to uniform the user interface specs for internal background projects, lower the unnecessary cost of design differences and implementation and liberate the resources of design and front-end development."
|
||||
class="ant-typography css-var-test-id ant-typography-ellipsis semantic-mark-root"
|
||||
style=""
|
||||
>
|
||||
Ant Design is a design language for background applications, refined by Ant UED Team. It aims to uniform the user interface specs for internal background projects, lower the unnecessary cost of design differences and implementation and liberate the resources of design and front-end development.
|
||||
<span
|
||||
class="ant-typography-actions semantic-mark-actions"
|
||||
>
|
||||
<button
|
||||
aria-label="Edit"
|
||||
class="ant-typography-edit semantic-mark-action"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="edit"
|
||||
class="anticon anticon-edit"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="edit"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Copy"
|
||||
class="ant-typography-copy semantic-mark-action"
|
||||
type="button"
|
||||
>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-col-8 css-var-test-id"
|
||||
>
|
||||
<ul
|
||||
class="acss-1ry21g5"
|
||||
>
|
||||
<li
|
||||
class="acss-1ehfz5v"
|
||||
>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-stretch ant-flex-gap-small ant-flex-vertical"
|
||||
>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-center ant-flex-justify-space-between ant-flex-gap-small"
|
||||
>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-center ant-flex-gap-small"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography css-var-test-id"
|
||||
style="margin: 0px;"
|
||||
>
|
||||
root
|
||||
</h5>
|
||||
<span
|
||||
class="ant-tag ant-tag-filled ant-tag-blue css-var-test-id"
|
||||
>
|
||||
6.4.0
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-center ant-flex-gap-small"
|
||||
>
|
||||
<button
|
||||
aria-hidden="true"
|
||||
class="ant-btn css-var-test-id ant-btn-default ant-btn-color-default ant-btn-variant-text ant-btn-sm ant-btn-icon-only"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="ant-btn-icon"
|
||||
>
|
||||
<span
|
||||
aria-label="pushpin"
|
||||
class="anticon anticon-pushpin"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="pushpin"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M878.3 392.1L631.9 145.7c-6.5-6.5-15-9.7-23.5-9.7s-17 3.2-23.5 9.7L423.8 306.9c-12.2-1.4-24.5-2-36.8-2-73.2 0-146.4 24.1-206.5 72.3a33.23 33.23 0 00-2.7 49.4l181.7 181.7-215.4 215.2a15.8 15.8 0 00-4.6 9.8l-3.4 37.2c-.9 9.4 6.6 17.4 15.9 17.4.5 0 1 0 1.5-.1l37.2-3.4c3.7-.3 7.2-2 9.8-4.6l215.4-215.4 181.7 181.7c6.5 6.5 15 9.7 23.5 9.7 9.7 0 19.3-4.2 25.9-12.4 56.3-70.3 79.7-158.3 70.2-243.4l161.1-161.1c12.9-12.8 12.9-33.8 0-46.8zM666.2 549.3l-24.5 24.5 3.8 34.4a259.92 259.92 0 01-30.4 153.9L262 408.8c12.9-7.1 26.3-13.1 40.3-17.9 27.2-9.4 55.7-14.1 84.7-14.1 9.6 0 19.3.5 28.9 1.6l34.4 3.8 24.5-24.5L608.5 224 800 415.5 666.2 549.3z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-hidden="true"
|
||||
class="ant-btn css-var-test-id ant-btn-text ant-btn-color-default ant-btn-variant-text ant-btn-sm ant-btn-icon-only"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="ant-btn-icon"
|
||||
>
|
||||
<span
|
||||
aria-label="info-circle"
|
||||
class="anticon anticon-info-circle"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="info-circle"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||
/>
|
||||
<path
|
||||
d="M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-typography css-var-test-id"
|
||||
style="margin: 0px; font-size: 12px;"
|
||||
>
|
||||
根元素,包含排版基础样式、布局和定位
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="acss-1ehfz5v"
|
||||
>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-stretch ant-flex-gap-small ant-flex-vertical"
|
||||
>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-center ant-flex-justify-space-between ant-flex-gap-small"
|
||||
>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-center ant-flex-gap-small"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography css-var-test-id"
|
||||
style="margin: 0px;"
|
||||
>
|
||||
actions
|
||||
</h5>
|
||||
<span
|
||||
class="ant-tag ant-tag-filled ant-tag-blue css-var-test-id"
|
||||
>
|
||||
6.4.0
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-center ant-flex-gap-small"
|
||||
>
|
||||
<button
|
||||
aria-hidden="true"
|
||||
class="ant-btn css-var-test-id ant-btn-default ant-btn-color-default ant-btn-variant-text ant-btn-sm ant-btn-icon-only"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="ant-btn-icon"
|
||||
>
|
||||
<span
|
||||
aria-label="pushpin"
|
||||
class="anticon anticon-pushpin"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="pushpin"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M878.3 392.1L631.9 145.7c-6.5-6.5-15-9.7-23.5-9.7s-17 3.2-23.5 9.7L423.8 306.9c-12.2-1.4-24.5-2-36.8-2-73.2 0-146.4 24.1-206.5 72.3a33.23 33.23 0 00-2.7 49.4l181.7 181.7-215.4 215.2a15.8 15.8 0 00-4.6 9.8l-3.4 37.2c-.9 9.4 6.6 17.4 15.9 17.4.5 0 1 0 1.5-.1l37.2-3.4c3.7-.3 7.2-2 9.8-4.6l215.4-215.4 181.7 181.7c6.5 6.5 15 9.7 23.5 9.7 9.7 0 19.3-4.2 25.9-12.4 56.3-70.3 79.7-158.3 70.2-243.4l161.1-161.1c12.9-12.8 12.9-33.8 0-46.8zM666.2 549.3l-24.5 24.5 3.8 34.4a259.92 259.92 0 01-30.4 153.9L262 408.8c12.9-7.1 26.3-13.1 40.3-17.9 27.2-9.4 55.7-14.1 84.7-14.1 9.6 0 19.3.5 28.9 1.6l34.4 3.8 24.5-24.5L608.5 224 800 415.5 666.2 549.3z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-hidden="true"
|
||||
class="ant-btn css-var-test-id ant-btn-text ant-btn-color-default ant-btn-variant-text ant-btn-sm ant-btn-icon-only"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="ant-btn-icon"
|
||||
>
|
||||
<span
|
||||
aria-label="info-circle"
|
||||
class="anticon anticon-info-circle"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="info-circle"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||
/>
|
||||
<path
|
||||
d="M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-typography css-var-test-id"
|
||||
style="margin: 0px; font-size: 12px;"
|
||||
>
|
||||
操作区域元素,包含复制、编辑、展开/收起等操作按钮的布局和间距样式
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="acss-1ehfz5v"
|
||||
>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-stretch ant-flex-gap-small ant-flex-vertical"
|
||||
>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-center ant-flex-justify-space-between ant-flex-gap-small"
|
||||
>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-center ant-flex-gap-small"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography css-var-test-id"
|
||||
style="margin: 0px;"
|
||||
>
|
||||
action
|
||||
</h5>
|
||||
<span
|
||||
class="ant-tag ant-tag-filled ant-tag-blue css-var-test-id"
|
||||
>
|
||||
6.4.0
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-flex css-var-test-id ant-flex-align-center ant-flex-gap-small"
|
||||
>
|
||||
<button
|
||||
aria-hidden="true"
|
||||
class="ant-btn css-var-test-id ant-btn-default ant-btn-color-default ant-btn-variant-text ant-btn-sm ant-btn-icon-only"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="ant-btn-icon"
|
||||
>
|
||||
<span
|
||||
aria-label="pushpin"
|
||||
class="anticon anticon-pushpin"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="pushpin"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M878.3 392.1L631.9 145.7c-6.5-6.5-15-9.7-23.5-9.7s-17 3.2-23.5 9.7L423.8 306.9c-12.2-1.4-24.5-2-36.8-2-73.2 0-146.4 24.1-206.5 72.3a33.23 33.23 0 00-2.7 49.4l181.7 181.7-215.4 215.2a15.8 15.8 0 00-4.6 9.8l-3.4 37.2c-.9 9.4 6.6 17.4 15.9 17.4.5 0 1 0 1.5-.1l37.2-3.4c3.7-.3 7.2-2 9.8-4.6l215.4-215.4 181.7 181.7c6.5 6.5 15 9.7 23.5 9.7 9.7 0 19.3-4.2 25.9-12.4 56.3-70.3 79.7-158.3 70.2-243.4l161.1-161.1c12.9-12.8 12.9-33.8 0-46.8zM666.2 549.3l-24.5 24.5 3.8 34.4a259.92 259.92 0 01-30.4 153.9L262 408.8c12.9-7.1 26.3-13.1 40.3-17.9 27.2-9.4 55.7-14.1 84.7-14.1 9.6 0 19.3.5 28.9 1.6l34.4 3.8 24.5-24.5L608.5 224 800 415.5 666.2 549.3z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-hidden="true"
|
||||
class="ant-btn css-var-test-id ant-btn-text ant-btn-color-default ant-btn-variant-text ant-btn-sm ant-btn-icon-only"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="ant-btn-icon"
|
||||
>
|
||||
<span
|
||||
aria-label="info-circle"
|
||||
class="anticon anticon-info-circle"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="info-circle"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||
/>
|
||||
<path
|
||||
d="M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-typography css-var-test-id"
|
||||
style="margin: 0px; font-size: 12px;"
|
||||
>
|
||||
单个操作按钮元素,包括复制、编辑、展开、收起按钮的样式,如内边距、圆角、颜色等
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,24 +2,24 @@
|
||||
|
||||
exports[`Typography rtl render component should be rendered correctly in RTL direction 1`] = `
|
||||
<div
|
||||
class="ant-typography ant-typography-rtl css-var-root"
|
||||
class="ant-typography css-var-root ant-typography-rtl"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Typography rtl render component should be rendered correctly in RTL direction 2`] = `
|
||||
<article
|
||||
class="ant-typography ant-typography-rtl css-var-root"
|
||||
class="ant-typography css-var-root ant-typography-rtl"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Typography rtl render component should be rendered correctly in RTL direction 3`] = `
|
||||
<h1
|
||||
class="ant-typography ant-typography-rtl css-var-root"
|
||||
class="ant-typography css-var-root ant-typography-rtl"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Typography rtl render component should be rendered correctly in RTL direction 4`] = `
|
||||
<a
|
||||
class="ant-typography ant-typography-rtl ant-typography-link css-var-root"
|
||||
class="ant-typography css-var-root ant-typography-rtl ant-typography-link"
|
||||
/>
|
||||
`;
|
||||
|
||||
3
components/typography/__tests__/demo-semantic.test.tsx
Normal file
3
components/typography/__tests__/demo-semantic.test.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { semanticDemoTest } from '../../../tests/shared/demoTest';
|
||||
|
||||
semanticDemoTest('typography');
|
||||
@@ -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!);
|
||||
});
|
||||
});
|
||||
|
||||
122
components/typography/__tests__/semantic.test.tsx
Normal file
122
components/typography/__tests__/semantic.test.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import Typography from '..';
|
||||
import { render } from '../../../tests/utils';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import type { TypographyClassNamesType, TypographyStylesType } from '../Base';
|
||||
|
||||
describe('Typography.Semantic', () => {
|
||||
it('should support classNames and styles for root, actions, and action', () => {
|
||||
const classNamesFn: TypographyClassNamesType = jest.fn(() => ({
|
||||
root: 'custom-typography',
|
||||
actions: 'custom-actions',
|
||||
action: 'custom-action',
|
||||
}));
|
||||
|
||||
const stylesFn: TypographyStylesType = jest.fn(() => ({
|
||||
root: { color: '#1890ff' },
|
||||
actions: { backgroundColor: '#f0f0f0' },
|
||||
action: { padding: '5px' },
|
||||
}));
|
||||
|
||||
const { rerender } = render(
|
||||
<Typography.Paragraph classNames={classNamesFn} styles={stylesFn} copyable>
|
||||
Test Typography
|
||||
</Typography.Paragraph>,
|
||||
);
|
||||
|
||||
expect(classNamesFn).toHaveBeenCalled();
|
||||
expect(stylesFn).toHaveBeenCalled();
|
||||
|
||||
const rootElement = document.querySelector<HTMLElement>('.ant-typography');
|
||||
expect(rootElement).toHaveClass('custom-typography');
|
||||
expect(rootElement).toHaveStyle({ color: 'rgb(24, 144, 255)' });
|
||||
|
||||
const actionsElement = document.querySelector<HTMLElement>('.ant-typography-actions');
|
||||
expect(actionsElement).toHaveClass('custom-actions');
|
||||
expect(actionsElement).toHaveStyle({ backgroundColor: 'rgb(240, 240, 240)' });
|
||||
|
||||
const actionButton = document.querySelector<HTMLElement>('.ant-typography-actions button');
|
||||
expect(actionButton).toHaveClass('custom-action');
|
||||
expect(actionButton).toHaveStyle({ padding: '5px' });
|
||||
|
||||
rerender(
|
||||
<Typography.Paragraph
|
||||
classNames={{
|
||||
root: 'obj-root',
|
||||
actions: 'obj-actions',
|
||||
action: 'obj-action',
|
||||
}}
|
||||
styles={{
|
||||
root: { fontSize: '16px', color: '#52c41a' },
|
||||
actions: { margin: '10px' },
|
||||
action: { borderRadius: '4px' },
|
||||
}}
|
||||
copyable
|
||||
>
|
||||
Updated Typography
|
||||
</Typography.Paragraph>,
|
||||
);
|
||||
|
||||
const updatedRootElement = document.querySelector<HTMLElement>('.ant-typography');
|
||||
expect(updatedRootElement).toHaveClass('obj-root');
|
||||
expect(updatedRootElement).toHaveStyle({ fontSize: '16px', color: 'rgb(82, 196, 26)' });
|
||||
|
||||
const updatedActionsElement = document.querySelector<HTMLElement>('.ant-typography-actions');
|
||||
expect(updatedActionsElement).toHaveClass('obj-actions');
|
||||
expect(updatedActionsElement).toHaveStyle({ margin: '10px' });
|
||||
|
||||
const updatedActionButton = document.querySelector<HTMLElement>(
|
||||
'.ant-typography-actions button',
|
||||
);
|
||||
expect(updatedActionButton).toHaveClass('obj-action');
|
||||
expect(updatedActionButton).toHaveStyle({ borderRadius: '4px' });
|
||||
});
|
||||
|
||||
it('should merge context and component classNames and styles', () => {
|
||||
const contextClassNames: TypographyClassNamesType = {
|
||||
root: 'context-root',
|
||||
actions: 'context-actions',
|
||||
};
|
||||
const contextStyles: TypographyStylesType = {
|
||||
root: { padding: '10px' },
|
||||
actions: { margin: '5px' },
|
||||
};
|
||||
const componentClassNames: TypographyClassNamesType = {
|
||||
root: 'component-root',
|
||||
};
|
||||
const componentStyles: TypographyStylesType = {
|
||||
root: { fontSize: '14px' },
|
||||
};
|
||||
|
||||
render(
|
||||
<ConfigProvider>
|
||||
<ConfigProvider
|
||||
typography={{
|
||||
className: undefined,
|
||||
style: undefined,
|
||||
classNames: contextClassNames,
|
||||
styles: contextStyles,
|
||||
}}
|
||||
>
|
||||
<Typography.Paragraph classNames={componentClassNames} styles={componentStyles} copyable>
|
||||
Test Typography
|
||||
</Typography.Paragraph>
|
||||
</ConfigProvider>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
|
||||
const rootElement = document.querySelector<HTMLElement>('.ant-typography');
|
||||
const actionsElement = document.querySelector<HTMLElement>('.ant-typography-actions');
|
||||
|
||||
expect(rootElement).toHaveClass(clsx(contextClassNames.root, componentClassNames.root));
|
||||
expect(actionsElement).toHaveClass(contextClassNames.actions!);
|
||||
|
||||
expect(rootElement).toHaveStyle({
|
||||
padding: contextStyles.root?.padding,
|
||||
fontSize: componentStyles.root?.fontSize,
|
||||
});
|
||||
expect(actionsElement).toHaveStyle({ margin: contextStyles.actions?.margin });
|
||||
});
|
||||
});
|
||||
43
components/typography/demo/_semantic.tsx
Normal file
43
components/typography/demo/_semantic.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
import useLocale from '../../../.dumi/hooks/useLocale';
|
||||
import SemanticPreview from '../../../.dumi/theme/common/SemanticPreview';
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
root: '根元素,包含排版基础样式、布局和定位',
|
||||
actions: '操作区域元素,包含复制、编辑、展开/收起等操作按钮的布局和间距样式',
|
||||
action: '单个操作按钮元素,包括复制、编辑、展开、收起按钮的样式,如内边距、圆角、颜色等',
|
||||
},
|
||||
en: {
|
||||
root: 'Root element with base typography styles, layout, and positioning',
|
||||
actions:
|
||||
'Actions element with layout and spacing styles for copy, edit, expand/collapse buttons',
|
||||
action:
|
||||
'Individual action button element including copy, edit, expand, collapse button styles like padding, border radius, colors, etc.',
|
||||
},
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [locale] = useLocale(locales);
|
||||
return (
|
||||
<SemanticPreview
|
||||
componentName="Typography"
|
||||
semantics={[
|
||||
{ name: 'root', desc: locale.root, version: '6.4.0' },
|
||||
{ name: 'actions', desc: locale.actions, version: '6.4.0' },
|
||||
{ name: 'action', desc: locale.action, version: '6.4.0' },
|
||||
]}
|
||||
>
|
||||
<Typography.Paragraph copyable editable ellipsis={{ rows: 2, expandable: true }}>
|
||||
Ant Design is a design language for background applications, refined by Ant UED Team. It
|
||||
aims to uniform the user interface specs for internal background projects, lower the
|
||||
unnecessary cost of design differences and implementation and liberate the resources of
|
||||
design and front-end development.
|
||||
</Typography.Paragraph>
|
||||
</SemanticPreview>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -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.
|
||||
|
||||
@@ -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 "Copy" / "Copied" 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
62
components/typography/hooks/useTypographySemantic.ts
Normal file
62
components/typography/hooks/useTypographySemantic.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useMergeSemantic } from '../../_util/hooks';
|
||||
import type { DirectionType } from '../../config-provider';
|
||||
import { useComponentConfig } from '../../config-provider/context';
|
||||
import type {
|
||||
BaseTypographyProps,
|
||||
TypographyClassNamesType,
|
||||
TypographySemanticClassNames,
|
||||
TypographySemanticStyles,
|
||||
TypographyStylesType,
|
||||
} from '../Base';
|
||||
|
||||
type UseTypographySemanticResult = [
|
||||
mergedClassNames: TypographySemanticClassNames,
|
||||
mergedStyles: TypographySemanticStyles,
|
||||
prefixCls: string,
|
||||
direction: DirectionType | undefined,
|
||||
];
|
||||
|
||||
export const useTypographySemantic = (
|
||||
customizePrefixCls?: string,
|
||||
classNames?: TypographyClassNamesType | undefined,
|
||||
styles?: TypographyStylesType | undefined,
|
||||
typographyDirection?: DirectionType,
|
||||
props?: BaseTypographyProps,
|
||||
): UseTypographySemanticResult => {
|
||||
const {
|
||||
getPrefixCls,
|
||||
direction: contextDirection,
|
||||
className: contextClassName,
|
||||
style: contextStyle,
|
||||
classNames: contextClassNames,
|
||||
styles: contextStyles,
|
||||
} = useComponentConfig('typography');
|
||||
|
||||
const direction = typographyDirection ?? contextDirection;
|
||||
const prefixCls = getPrefixCls('typography', customizePrefixCls);
|
||||
|
||||
const mergedProps: BaseTypographyProps = {
|
||||
...props,
|
||||
prefixCls,
|
||||
direction,
|
||||
};
|
||||
|
||||
const contextClassNamesObject = useMemo(() => ({ root: contextClassName }), [contextClassName]);
|
||||
const contextStylesObject = useMemo(() => ({ root: contextStyle }), [contextStyle]);
|
||||
|
||||
const [mergedClassNames, mergedStyles] = useMergeSemantic<
|
||||
TypographyClassNamesType,
|
||||
TypographyStylesType,
|
||||
BaseTypographyProps
|
||||
>(
|
||||
[contextClassNamesObject, contextClassNames, classNames],
|
||||
[contextStylesObject, contextStyles, styles],
|
||||
{
|
||||
props: mergedProps,
|
||||
},
|
||||
);
|
||||
|
||||
return [mergedClassNames, mergedStyles, prefixCls, direction];
|
||||
};
|
||||
@@ -4,14 +4,16 @@ import Text from './Text';
|
||||
import Title from './Title';
|
||||
import OriginTypography from './Typography';
|
||||
|
||||
export type TypographyProps = typeof OriginTypography & {
|
||||
export type { TypographyProps } from './Typography';
|
||||
|
||||
type CompoundedComponent = typeof OriginTypography & {
|
||||
Text: typeof Text;
|
||||
Link: typeof Link;
|
||||
Title: typeof Title;
|
||||
Paragraph: typeof Paragraph;
|
||||
};
|
||||
|
||||
const Typography = OriginTypography as TypographyProps;
|
||||
const Typography = OriginTypography as CompoundedComponent;
|
||||
Typography.Text = Text;
|
||||
Typography.Link = Link;
|
||||
Typography.Title = Title;
|
||||
|
||||
@@ -107,6 +107,10 @@ const genTypographyStyle: GenerateStyle<TypographyToken> = (token) => {
|
||||
...getLinkStyles(token),
|
||||
|
||||
// Operation
|
||||
[`${componentCls}-actions`]: {
|
||||
display: 'inline',
|
||||
},
|
||||
|
||||
[`
|
||||
${componentCls}-expand,
|
||||
${componentCls}-collapse,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user