mirror of
https://github.com/ant-design/ant-design.git
synced 2026-02-12 12:29:20 +08:00
Compare commits
6 Commits
docs/theme
...
feature
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0088e0221c | ||
|
|
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 };
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user