Compare commits

..

1 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
0b184c40e2 Initial plan 2026-01-08 01:01:28 +00:00
900 changed files with 30699 additions and 85782 deletions

View File

@@ -7,6 +7,7 @@ const isNumber = (value: any): value is number => {
};
const fetcher = async (url: string): Promise<number> => {
// eslint-disable-next-line compat/compat
const res = await fetch(url, { headers: { Accept: 'application/vnd.github+json' } });
const data = await res.json();
const totalCount = isNumber(data?.total_count) ? data.total_count : 0;
@@ -31,7 +32,7 @@ export const useIssueCount = (options: UseIssueCountOptions) => {
// Note: current query only filters by title keywords. Filtering by component name can be added later if needed.
const searchUrl = useMemo(() => {
const tokens = (titleKeywords || []).filter(Boolean).map<string>(encodeURIComponent);
const tokens = (titleKeywords || []).filter(Boolean).map((k) => encodeURIComponent(String(k)));
const orExpr = tokens.length > 0 ? tokens.join('%20OR%20') : '';
const titlePart = orExpr ? `in:title+(${orExpr})` : 'in:title';
const q = `repo:${repo}+is:issue+is:open+${titlePart}`;
@@ -45,7 +46,7 @@ export const useIssueCount = (options: UseIssueCountOptions) => {
const issueNewUrl = `https://github.com/${repo}/issues/new/choose`;
const issueSearchUrl = useMemo(() => {
const keywords = (titleKeywords || []).filter(Boolean).map<string>(String);
const keywords = (titleKeywords || []).filter(Boolean).map((k) => String(k));
const groupExpr =
keywords.length > 0 ? `(${keywords.map((k) => `is:issue in:title ${k}`).join(' OR ')})` : '';
const qRaw = `is:open ${groupExpr}`.trim();

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import type { MenuProps } from 'antd';
import { Flex, Tag, version } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { clsx } from 'clsx';
import { useFullSidebarData, useSidebarData } from 'dumi';
@@ -33,7 +33,7 @@ const getTagColor = (val?: string) => {
}
};
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ css, cssVar }) => ({
link: css`
display: flex;
align-items: center;
@@ -62,6 +62,7 @@ interface MenuItemLabelProps {
}
const MenuItemLabelWithTag: React.FC<MenuItemLabelProps> = (props) => {
const { styles } = useStyle();
const { before, after, link, title, subtitle, search, tag, className } = props;
const [locale] = useLocale(locales);

View File

@@ -3,14 +3,10 @@ import { removeCSS, updateCSS } from '@rc-component/util/lib/Dom/dynamicCSS';
import theme from '../../components/theme';
const duration = 0.5;
const viewTransitionStyle = `
@keyframes keepAlive {100% { z-index: -1 }}
::view-transition-old(root),
::view-transition-new(root) {
animation: keepAlive ${duration}s linear;
animation-fill-mode: forwards;
animation: none;
mix-blend-mode: normal;
}
@@ -54,7 +50,7 @@ const useThemeAnimation = () => {
clipPath: isDark ? [...clipPath].reverse() : clipPath,
},
{
duration: duration * 1000,
duration: 500,
easing: 'ease-in',
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)',
},

View File

@@ -1,3 +0,0 @@
@layer antd {
@import '~../components/style/antd.css';
}

View File

@@ -1,5 +1,4 @@
import React from 'react';
import raf from '@rc-component/util/lib/raf';
import { Alert, Badge, Carousel, Flex, Skeleton, Typography } from 'antd';
import { createStyles } from 'antd-style';
import { clsx } from 'clsx';
@@ -19,46 +18,12 @@ const useStyle = createStyles(({ cssVar, css, cx }) => {
align-items: stretch;
text-decoration: none;
background: ${cssVar.colorBgContainer};
background: color-mix(in srgb, ${cssVar.colorBgContainer} 30%, transparent);
backdrop-filter: blur(8px);
border: ${cssVar.lineWidth} solid ${cssVar.colorBorderSecondary};
border-radius: ${cssVar.borderRadiusLG};
transition: all ${cssVar.motionDurationSlow};
padding-block: ${cssVar.paddingMD};
padding-inline: ${cssVar.paddingLG};
box-sizing: border-box;
position: relative;
&:before {
content: '';
inset: calc(${cssVar.lineWidth} * -1);
position: absolute;
background: radial-gradient(
circle 150px at var(--mouse-x, 0) var(--mouse-y, 0),
${cssVar.colorPrimaryBorderHover},
${cssVar.colorBorderSecondary}
);
opacity: 0;
transition: all 0.3s ease;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: subtract;
-webkit-mask-composite: xor;
padding: 1px;
border-radius: inherit;
}
&:hover {
backdrop-filter: blur(0px);
background: color-mix(in srgb, ${cssVar.colorBgContainer} 90%, transparent);
&:before {
opacity: 1;
}
}
`;
return {
@@ -68,6 +33,12 @@ const useStyle = createStyles(({ cssVar, css, cx }) => {
height: 100%;
}
`,
cardItem: css`
&:hover {
box-shadow: ${cssVar.boxShadowCard};
border-color: transparent;
}
`,
sliderItem: css`
margin: 0 ${cssVar.margin};
text-align: start;
@@ -93,9 +64,6 @@ const useStyle = createStyles(({ cssVar, css, cx }) => {
};
});
// ======================================================================
// == Item ==
// ======================================================================
interface RecommendItemProps {
extra: Extra;
index: number;
@@ -105,49 +73,9 @@ interface RecommendItemProps {
const RecommendItem: React.FC<RecommendItemProps> = (props) => {
const { extra, index, icons, className } = props;
const cardRef = React.useRef<HTMLAnchorElement>(null);
const { styles } = useStyle();
// ====================== MousePos ======================
const [mousePosition, setMousePosition] = React.useState<[number, number]>([0, 0]);
const [transMousePosition, setTransMousePosition] = React.useState<[number, number]>([0, 0]);
const onMouseMove: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
if (!cardRef.current) {
return;
}
const rect = cardRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setMousePosition([x, y]);
};
// Transition mouse position
React.useEffect(() => {
const [targetX, targetY] = mousePosition;
const [currentX, currentY] = transMousePosition;
if (Math.abs(targetX - currentX) < 0.5 && Math.abs(targetY - currentY) < 0.5) {
return;
}
const rafId = raf(() => {
setTransMousePosition((ori) => {
const [curX, curY] = ori;
const deltaX = (targetX - curX) * 0.1;
const deltaY = (targetY - curY) * 0.1;
return [curX + deltaX, curY + deltaY];
});
});
return () => raf.cancel(rafId);
}, [mousePosition, transMousePosition]);
// ======================= Render =======================
if (!extra) {
return <Skeleton key={index} />;
}
@@ -156,19 +84,11 @@ const RecommendItem: React.FC<RecommendItemProps> = (props) => {
const card = (
<a
ref={cardRef}
key={extra?.title}
href={extra.href}
target="_blank"
className={clsx(styles.itemBase, className)}
style={
{
'--mouse-x': `${transMousePosition[0]}px`,
'--mouse-y': `${transMousePosition[1]}px`,
} as React.CSSProperties
}
rel="noreferrer"
onMouseMove={onMouseMove}
>
<Typography.Title level={5}>{extra?.title}</Typography.Title>
<Typography.Paragraph type="secondary" style={{ flex: 'auto' }}>
@@ -194,9 +114,6 @@ const RecommendItem: React.FC<RecommendItemProps> = (props) => {
return card;
};
// ======================================================================
// == Fallback ==
// ======================================================================
export const BannerRecommendsFallback: React.FC = () => {
const { isMobile } = React.use(SiteContext);
@@ -223,9 +140,6 @@ export const BannerRecommendsFallback: React.FC = () => {
);
};
// ======================================================================
// == Recommends ==
// ======================================================================
const BannerRecommends: React.FC = () => {
const { styles } = useStyle();
const [, lang] = useLocale();
@@ -272,7 +186,13 @@ const BannerRecommends: React.FC = () => {
return (
<div className={styles.container}>
{mergedExtras.map((extra, index) => (
<RecommendItem key={`desktop-${index}`} extra={extra} index={index} icons={data?.icons} />
<RecommendItem
key={`desktop-${index}`}
extra={extra}
index={index}
icons={data?.icons}
className={styles.cardItem}
/>
))}
</div>
);

View File

@@ -1,18 +1,15 @@
import * as React from 'react';
import { Typography } from 'antd';
import { createStaticStyles, useTheme } from 'antd-style';
import { createStyles, useTheme } from 'antd-style';
import { clsx } from 'clsx';
import SiteContext from '../../../theme/slots/SiteContext';
import GroupMaskLayer from './GroupMaskLayer';
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ css, cssVar }) => ({
box: css`
position: relative;
transition: all ${cssVar.motionDurationSlow};
background-size: cover;
background-position: 50% 0%;
background-repeat: no-repeat;
`,
container: css`
position: absolute;
@@ -44,46 +41,15 @@ export interface GroupProps {
/** 是否不使用两侧 margin */
collapse?: boolean;
decoration?: React.ReactNode;
/** 预加载的背景图片列表 */
backgroundPrefetchList?: string[];
}
const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
const {
id,
title,
titleColor,
description,
children,
decoration,
background,
collapse,
backgroundPrefetchList,
} = props;
// 预加载背景图片
React.useEffect(() => {
if (backgroundPrefetchList && backgroundPrefetchList.length > 0) {
backgroundPrefetchList.forEach((url) => {
if (url && url.startsWith('https')) {
const img = new Image();
img.src = url;
}
});
}
}, [backgroundPrefetchList]);
const { id, title, titleColor, description, children, decoration, background, collapse } = props;
const token = useTheme();
const { styles } = useStyle();
const { isMobile } = React.use(SiteContext);
return (
<div
style={
background?.startsWith('https')
? { backgroundImage: `url(${background})` }
: { backgroundColor: background }
}
className={styles.box}
>
<div style={{ backgroundColor: background }} className={styles.box}>
<div className={styles.container}>{decoration}</div>
<GroupMaskLayer style={{ paddingBlock: token.marginFarSM }}>
<div className={styles.typographyWrapper}>

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { clsx } from 'clsx';
const classNames = createStaticStyles(({ css }) => ({
const useStyle = createStyles(({ css }) => ({
siteMask: css`
z-index: 1;
position: relative;
@@ -19,10 +19,11 @@ export interface GroupMaskLayerProps {
const GroupMaskLayer: React.FC<React.PropsWithChildren<GroupMaskLayerProps>> = (props) => {
const { children, className, style, onMouseMove, onMouseEnter, onMouseLeave } = props;
const { styles } = useStyle();
return (
<div
style={style}
className={clsx(className, classNames.siteMask)}
className={clsx(className, styles.siteMask)}
onMouseMove={onMouseMove}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}

View File

@@ -17,7 +17,7 @@ import {
Switch,
Tooltip,
} from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import useLocale from '../../../../hooks/useLocale';
import Tilt from './Tilt';
@@ -71,7 +71,7 @@ const locales = {
},
};
const styles = createStaticStyles(({ cssVar, css }) => {
const useStyle = createStyles(({ cssVar, css }) => {
const gap = cssVar.padding;
return {
holder: css`
@@ -106,6 +106,7 @@ const styles = createStaticStyles(({ cssVar, css }) => {
const ComponentsBlock: React.FC = () => {
const [locale] = useLocale(locales);
const { styles } = useStyle();
return (
<Tilt options={{ max: 4, glare: false, scale: 0.98 }} className={styles.holder}>

View File

@@ -1,115 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useEvent } from '@rc-component/util';
import { createStyles } from 'antd-style';
import { DarkContext } from '../../../../hooks/useDark';
interface BubbleProps {
size: number | string;
left?: number | string;
top?: number | string;
color: string;
offsetXMultiple?: number;
offsetYMultiple?: number;
defaultOpacity?: number;
}
const MAX_OFFSET = 200;
const Bubble = ({
size,
left,
top,
color,
offsetXMultiple = 1,
offsetYMultiple = 1,
defaultOpacity = 0.1,
}: BubbleProps) => {
const [offset, setOffset] = useState([0, 0]);
const [opacity, setOpacity] = useState(defaultOpacity);
const [sizeOffset, setSizeOffset] = useState(1);
const isDark = React.use(DarkContext);
const randomPos = useEvent(() => {
const baseOffsetX = (Math.random() - 0.5) * MAX_OFFSET * 2 * offsetXMultiple;
const baseOffsetY = (Math.random() - 0.5) * MAX_OFFSET * 2 * offsetYMultiple;
setOffset([baseOffsetX, baseOffsetY]);
setOpacity(isDark ? 0.1 + Math.random() * 0.2 : 0.1 + Math.random() * 0.05);
setSizeOffset(1 + Math.random() * 1);
});
useEffect(() => {
randomPos();
}, []);
useEffect(() => {
const randomTimeout = Math.random() * 2000 + 3000;
const id = setTimeout(randomPos, randomTimeout);
return () => clearTimeout(id);
}, [offset]);
return (
<div
aria-hidden="true"
data-desc="luminous-bubble"
style={{
opacity,
width: size,
height: size,
borderRadius: '50%',
background: color,
filter: 'blur(100px)',
left,
top,
transform: `translate(-50%, -50%) translate(${offset[0]}px, ${offset[1]}px) scale(${sizeOffset})`,
transition: 'all 5s ease-in-out',
position: 'absolute',
}}
/>
);
};
const useStyles = createStyles(({ css, cssVar }) => ({
container: css`
position: absolute;
inset: 0;
overflow: hidden;
background: ${cssVar.colorBgContainer};
`,
}));
interface LuminousBgProps {
className?: string;
}
export default function LuminousBg({ className }: LuminousBgProps) {
const { styles, cx } = useStyles();
return (
<div className={cx(styles.container, className)}>
{/* Left + Top */}
<Bubble
size={300}
color="#ee35f1"
left="0vw"
top="0vh"
offsetXMultiple={2}
defaultOpacity={0.2}
/>
{/* Left + Bottom */}
<Bubble size={300} color="#5939dc" left="30vw" top="80vh" defaultOpacity={0.1} />
{/* Right + Middle */}
<Bubble
size={300}
color="#00D6FF"
left="100vw"
top="50vh"
offsetYMultiple={2}
defaultOpacity={0.2}
/>
</div>
);
}

View File

@@ -1,34 +1,48 @@
import React, { Suspense, use } from 'react';
import { Flex, Typography } from 'antd';
import { createStyles } from 'antd-style';
import { clsx } from 'clsx';
import { useLocation } from 'dumi';
import useLocale from '../../../../hooks/useLocale';
import LinkButton from '../../../../theme/common/LinkButton';
import SiteContext from '../../../../theme/slots/SiteContext';
import type { SiteContextProps } from '../../../../theme/slots/SiteContext';
import * as utils from '../../../../theme/utils';
import GroupMaskLayer from '../GroupMaskLayer';
import '../SiteContext';
import LuminousBg from './LuminousBg';
const ComponentsBlock = React.lazy(() => import('./ComponentsBlock'));
const locales = {
cn: {
slogan: 'AI友好的「设计系统」让美与智能并进让工作充满「灵感」与「快乐」。',
slogan: '助力设计开发者「更灵活」地搭建出「更美」的产品,让用户「快乐工作」~',
start: '开始使用',
designLanguage: '设计语言',
},
en: {
slogan:
'AI friendly design system that combines beauty and intelligence, making work full of inspiration and joy.',
'Help designers/developers building beautiful products more flexible and working with happiness',
start: 'Getting Started',
designLanguage: 'Design Language',
},
};
const useStyle = createStyles(({ cssVar, css, cx }) => {
const useStyle = createStyles(({ cssVar, css, cx }, siteConfig: SiteContextProps) => {
const textShadow = `0 0 4px ${cssVar.colorBgContainer}`;
const mask = cx(css`
position: absolute;
inset: 0;
backdrop-filter: blur(2px);
opacity: 1;
background-color: rgba(255, 255, 255, 0.2);
transition: all 1s ease;
pointer-events: none;
[data-prefers-color='dark'] & {
background-color: rgba(0, 0, 0, 0.2);
}
`);
const block = cx(css`
position: absolute;
@@ -52,12 +66,18 @@ const useStyle = createStyles(({ cssVar, css, cx }) => {
row-gap: ${cssVar.marginXL};
&:hover {
.${mask} {
opacity: 0;
}
.${block} {
transform: scale(0.96);
}
}
`,
mask,
typography: css`
text-align: center;
position: relative;
@@ -66,15 +86,14 @@ const useStyle = createStyles(({ cssVar, css, cx }) => {
text-shadow: ${Array.from({ length: 5 }, () => textShadow).join(', ')};
h1 {
font-weight: 900 !important;
font-size: calc(${cssVar.fontSizeHeading1} * 2) !important;
line-height: ${cssVar.lineHeightHeading1} !important;
font-size: calc(${cssVar.fontSizeHeading2} * 2) !important;
line-height: ${cssVar.lineHeightHeading2} !important;
}
p {
font-size: calc(${cssVar.fontSizeLG} * 1.5) !important;
font-weight: 400 !important;
font-size: ${cssVar.fontSizeLG} !important;
font-weight: normal !important;
margin-bottom: 0;
color: ${cssVar.colorTextTertiary} !important;
}
`,
block,
@@ -88,6 +107,18 @@ const useStyle = createStyles(({ cssVar, css, cx }) => {
btnWrap: css`
margin-bottom: ${cssVar.marginXL};
`,
bgImg: css`
position: absolute;
width: 240px;
`,
bgImgTop: css`
top: 0;
inset-inline-start: ${siteConfig.isMobile ? '-120px' : 0};
`,
bgImgBottom: css`
bottom: 120px;
inset-inline-end: ${siteConfig.isMobile ? 0 : '40%'};
`,
};
});
@@ -101,11 +132,31 @@ const PreviewBanner: React.FC<Readonly<React.PropsWithChildren>> = (props) => {
return (
<GroupMaskLayer>
{/* Image Left Top */}
<img
alt="bg"
src="https://gw.alipayobjects.com/zos/bmw-prod/49f963db-b2a8-4f15-857a-270d771a1204.svg"
draggable={false}
className={clsx(styles.bgImg, styles.bgImgTop)}
/>
{/* Image Right Top */}
<img
alt="bg"
src="https://gw.alipayobjects.com/zos/bmw-prod/e152223c-bcae-4913-8938-54fda9efe330.svg"
draggable={false}
className={clsx(styles.bgImg, styles.bgImgBottom)}
/>
<div className={styles.holder}>
{/* Mobile not show the component preview */}
<Suspense fallback={null}>
<LuminousBg />
{siteConfig.isMobile ? null : (
<div className={styles.block}>
<ComponentsBlock />
</div>
)}
</Suspense>
<div className={styles.mask} />
<Typography className={styles.typography}>
<h1>Ant Design</h1>
<p>{locale.slogan}</p>

View File

@@ -1,6 +1,6 @@
import React, { useMemo, useState } from 'react';
import { CSSMotionList } from '@rc-component/motion';
import { createStaticStyles } from 'antd-style';
import { createStyles, css } from 'antd-style';
import { clsx } from 'clsx';
import { COLOR_IMAGES, getClosetColor } from './colorUtil';
@@ -10,7 +10,7 @@ export interface BackgroundImageProps {
isLight?: boolean;
}
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ cssVar }) => ({
image: css`
transition: all ${cssVar.motionDurationSlow};
position: absolute;
@@ -29,6 +29,7 @@ const onHide = () => ({ opacity: 0 });
const BackgroundImage: React.FC<BackgroundImageProps> = ({ colorPrimary, isLight }) => {
const activeColor = useMemo(() => getClosetColor(colorPrimary), [colorPrimary]);
const { styles } = useStyle();
const [keyList, setKeyList] = useState<string[]>([]);

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { ColorPicker, Flex, Input } from 'antd';
import type { ColorPickerProps, GetProp } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { generateColor } from 'antd/es/color-picker/util';
import { clsx } from 'clsx';
@@ -9,7 +9,7 @@ import { PRESET_COLORS } from './colorUtil';
type Color = Extract<GetProp<ColorPickerProps, 'value'>, string | { cleared: any }>;
const styles = createStaticStyles(({ cssVar, css }) => ({
const useStyle = createStyles(({ cssVar, css }) => ({
color: css`
width: calc(${cssVar.controlHeightLG} / 2);
height: calc(${cssVar.controlHeightLG} / 2);
@@ -69,6 +69,8 @@ const DebouncedColorPicker: React.FC<React.PropsWithChildren<ThemeColorPickerPro
};
const ThemeColorPicker: React.FC<ThemeColorPickerProps> = ({ value, onChange, id }) => {
const { styles } = useStyle();
const matchColors = React.useMemo(() => {
const valueStr = generateColor(value || '').toRgbString();
const colors = PRESET_COLORS.map((color) => {

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { Flex } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { clsx } from 'clsx';
import useLocale from '../../../../hooks/useLocale';
@@ -32,7 +32,7 @@ const locales = {
},
};
const styles = createStaticStyles(({ cssVar, css }) => ({
const useStyle = createStyles(({ cssVar, css }) => ({
themeCard: css`
border-radius: ${cssVar.borderRadius};
cursor: pointer;
@@ -80,6 +80,7 @@ export interface ThemePickerProps {
const ThemePicker: React.FC<ThemePickerProps> = (props) => {
const { value, id, onChange } = props;
const { styles } = useStyle();
const [locale] = useLocale(locales);
return (
<Flex gap="large" wrap>

View File

@@ -20,7 +20,7 @@ import {
theme,
Typography,
} from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { generateColor } from 'antd/es/color-picker/util';
import { clsx } from 'clsx';
import { useLocation } from 'dumi';
@@ -92,7 +92,7 @@ const locales = {
};
// ============================= Style =============================
const styles = createStaticStyles(({ cssVar, css, cx }) => {
const useStyle = createStyles(({ cssVar, css, cx }) => {
const { carousel } = getCarouselStyle();
const demo = css`
overflow: hidden;
@@ -345,6 +345,7 @@ function rgbToColorMatrix(color: string) {
}
const Theme: React.FC = () => {
const { styles } = useStyle();
const [locale, lang] = useLocale(locales);
const isZhCN = lang === 'cn';
const { search } = useLocation();

View File

@@ -1,198 +0,0 @@
import React from 'react';
import { CheckOutlined, CloseOutlined, DownOutlined } from '@ant-design/icons';
import {
Alert,
App,
Button,
Card,
Checkbox,
ColorPicker,
ConfigProvider,
Dropdown,
Flex,
Modal,
Progress,
Radio,
Select,
Slider,
Space,
Steps,
Switch,
} from 'antd';
import type { ConfigProviderProps } from 'antd';
import { createStyles } from 'antd-style';
import clsx from 'clsx';
import useLocale from '../../../../hooks/useLocale';
const { _InternalPanelDoNotUseOrYouWillBeFired: ModalPanel } = Modal;
const locales = {
cn: {
range: '设置范围',
text: 'Ant Design 使用 CSS-in-JS 技术以提供动态与混合主题的能力。与此同时,我们使用组件级别的 CSS-in-JS 解决方案,让你的应用获得更好的性能。',
infoText: '信息内容展示',
dropdown: '下拉菜单',
finished: '已完成',
inProgress: '进行中',
waiting: '等待中',
option: '选项',
apple: '苹果',
banana: '香蕉',
orange: '橘子',
watermelon: '西瓜',
primary: '主要按钮',
danger: '危险按钮',
default: '默认按钮',
dashed: '虚线按钮',
icon: '图标按钮',
hello: '你好Ant Design!',
release: 'Ant Design 6.0 正式发布!',
},
en: {
range: 'Set Range',
text: 'Ant Design use CSS-in-JS technology to provide dynamic & mix theme ability. And which use component level CSS-in-JS solution get your application a better performance.',
infoText: 'Info Text',
dropdown: 'Dropdown',
finished: 'Finished',
inProgress: 'In Progress',
waiting: 'Waiting',
option: 'Option',
apple: 'Apple',
banana: 'Banana',
orange: 'Orange',
watermelon: 'Watermelon',
primary: 'Primary',
danger: 'Danger',
default: 'Default',
dashed: 'Dashed',
icon: 'Icon',
hello: 'Hello, Ant Design!',
release: 'Ant Design 6.0 is released!',
},
};
const useStyle = createStyles(({ css, cssVar }) => {
return {
container: css({
backgroundColor: `color-mix(in srgb, ${cssVar.colorBgContainer} 70%, transparent)`,
backdropFilter: 'blur(12px)',
}),
flexAuto: css({ flex: 'auto' }),
};
});
interface ComponentsBlockProps {
config?: ConfigProviderProps;
style?: React.CSSProperties;
className?: string;
containerClassName?: string;
}
const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
const [locale] = useLocale(locales);
const { styles } = useStyle();
const { config, style, className, containerClassName } = props;
return (
<ConfigProvider {...config}>
<Card className={clsx(containerClassName, styles.container)}>
<App>
<Flex vertical gap="middle" style={style} className={className}>
<ModalPanel title="Ant Design" width="100%">
{locale.text}
</ModalPanel>
<Alert title={locale.infoText} type="info" />
{/* Line */}
<Flex gap="middle">
<div style={{ flex: 'none' }}>
<Space.Compact>
<Button>{locale.dropdown}</Button>
<Dropdown
menu={{
items: Array.from({ length: 5 }).map((_, index) => ({
key: `opt${index}`,
label: `${locale.option} ${index}`,
})),
}}
>
<Button icon={<DownOutlined />} />
</Dropdown>
</Space.Compact>
</div>
<ColorPicker style={{ flex: 'none' }} />
<Select
style={{ flex: 'auto' }}
mode="multiple"
maxTagCount="responsive"
defaultValue={[{ value: 'apple' }, { value: 'banana' }]}
options={[
{ value: 'apple', label: locale.apple },
{ value: 'banana', label: locale.banana },
{ value: 'orange', label: locale.orange },
{ value: 'watermelon', label: locale.watermelon },
]}
/>
</Flex>
<Progress style={{ margin: 0 }} percent={60} />
<Steps
current={1}
items={[
{ title: locale.finished },
{ title: locale.inProgress },
{ title: locale.waiting },
]}
/>
{/* Line */}
<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}>
{locale.primary}
</Button>
<Button type="primary" className={styles.flexAuto} danger>
{locale.danger}
</Button>
<Button className={styles.flexAuto}>{locale.default}</Button>
<Button className={styles.flexAuto} type="dashed">
{locale.dashed}
</Button>
</Flex>
{/* Line */}
<Flex gap="middle">
<Switch
defaultChecked
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
style={{ width: 48 }}
/>
<Checkbox.Group
options={[locale.apple, locale.banana, locale.orange]}
defaultValue={[locale.apple]}
/>
<Radio.Group defaultValue={locale.apple} options={[locale.apple, locale.banana]} />
</Flex>
</Flex>
</App>
</Card>
</ConfigProvider>
);
};
export default ComponentsBlock;

View File

@@ -1,180 +0,0 @@
import * as React from 'react';
import { ConfigProvider, Flex, theme } from 'antd';
import { createStyles } from 'antd-style';
import clsx from 'clsx';
import { DarkContext } from '../../../../hooks/useDark';
import useLocale from '../../../../hooks/useLocale';
import Group from '../Group';
import ComponentsBlock from './ComponentsBlock';
import usePreviewThemes from './previewThemes';
const locales = {
cn: {
themeTitle: '定制主题,随心所欲',
themeDesc: '开放样式算法与语义化结构,让你与 AI 一起轻松定制主题',
},
en: {
themeTitle: 'Flexible theme customization',
themeDesc:
'Open style algorithms and semantic structures make it easy for you and AI to customize themes',
},
};
const useStyles = createStyles(({ css, cssVar }) => ({
container: css({
width: '100%',
color: cssVar.colorText,
lineHeight: cssVar.lineHeight,
fontSize: cssVar.fontSize,
fontFamily: cssVar.fontFamily,
alignItems: 'stretch',
justifyContent: 'center',
}),
// List
list: css({
flex: 'auto',
margin: 0,
padding: 0,
listStyleType: 'none',
display: 'flex',
flexDirection: 'column',
gap: cssVar.paddingMD,
}),
listItem: css({
margin: 0,
fontSize: cssVar.fontSizeLG,
lineHeight: cssVar.lineHeightLG,
paddingBlock: cssVar.padding,
paddingInline: cssVar.paddingLG,
border: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorBorderSecondary}`,
borderRadius: cssVar.borderRadius,
borderColor: 'transparent',
transition: `all ${cssVar.motionDurationMid} ${cssVar.motionEaseInOut}`,
'&:hover:not(.active)': {
borderColor: cssVar.colorPrimaryBorder,
backgroundColor: cssVar.colorPrimaryBg,
cursor: 'pointer',
},
'&:focus-visible': {
outline: `2px solid ${cssVar.colorPrimary}`,
outlineOffset: 2,
},
'&.active': {
borderColor: cssVar.colorPrimary,
backgroundColor: cssVar.colorPrimaryBg,
color: cssVar.colorPrimary,
},
// ========= Dark =========
'&.dark': {
color: cssVar.colorTextLightSolid,
backgroundColor: 'transparent',
'&:hover, &.active': {
borderColor: cssVar.colorTextLightSolid,
backgroundColor: 'transparent',
},
},
}),
// Components
componentsBlockContainer: css({
flex: 'auto',
display: 'flex',
padding: cssVar.paddingXL,
justifyContent: 'center',
border: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorBorderSecondary}`,
borderRadius: cssVar.borderRadius,
boxShadow: cssVar.boxShadow,
}),
componentsBlock: css({
flex: 'none',
maxWidth: `calc(420px + ${cssVar.paddingXL} * 2)`,
}),
}));
export default function ThemePreview() {
const [locale] = useLocale(locales);
const { styles } = useStyles();
const isDark = React.use(DarkContext);
const previewThemes = usePreviewThemes();
const [activeName, setActiveName] = React.useState(() => previewThemes[0].name);
React.useEffect(() => {
const defaultThemeName = isDark ? 'dark' : 'light';
const targetTheme =
process.env.NODE_ENV !== 'production'
? previewThemes[previewThemes.length - 1].name
: previewThemes.find((theme) => theme.key === defaultThemeName)?.name ||
previewThemes[0].name;
setActiveName(targetTheme);
}, [isDark]);
// 收集所有背景图片用于预加载
const backgroundPrefetchList = React.useMemo(
() => previewThemes.map((theme) => theme.bgImg).filter((img): img is string => !!img),
[previewThemes],
);
const handleThemeClick = (name: string) => {
setActiveName(name);
};
const handleKeyDown = (event: React.KeyboardEvent, name: string) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleThemeClick(name);
}
};
const activeTheme = previewThemes.find((theme) => theme.name === activeName);
return (
<ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
<Group
title={locale.themeTitle}
description={locale.themeDesc}
background={activeTheme?.bgImg}
titleColor={activeTheme?.bgImgDark ? '#fff' : undefined}
backgroundPrefetchList={backgroundPrefetchList}
>
<Flex className={styles.container} gap="large">
<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)}
>
{theme.name}
</div>
))}
</div>
<ComponentsBlock
key={activeName}
config={activeTheme?.props}
className={styles.componentsBlock}
containerClassName={styles.componentsBlockContainer}
/>
</Flex>
</Group>
</ConfigProvider>
);
}

View File

@@ -1,188 +0,0 @@
import { useMemo } from 'react';
import { theme } from 'antd';
import type { ConfigProviderProps } from 'antd';
import { createStyles } from 'antd-style';
import clsx from 'clsx';
import type { UseTheme } from '.';
const useStyles = createStyles(({ css, cssVar }) => {
return {
boxBorder: css({
border: `${cssVar.lineWidth} ${cssVar.lineType} color-mix(in srgb,${cssVar.colorBorder} 80%, #000)`,
}),
alertRoot: css({
color: cssVar.colorInfoText,
textShadow: `0 1px 0 rgba(255, 255, 255, 0.8)`,
}),
modalContainer: css({
padding: 0,
borderRadius: cssVar.borderRadiusLG,
}),
modalHeader: css({
borderBottom: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorSplit}`,
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
}),
modalBody: css({
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
}),
modalFooter: css({
borderTop: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorSplit}`,
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
backgroundColor: cssVar.colorBgContainerDisabled,
boxShadow: `inset 0 1px 0 ${cssVar.colorBgContainer}`,
}),
buttonRoot: css({
backgroundImage: `linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.2))`,
boxShadow: `inset 0 1px 0 rgba(255, 255, 255, 0.15)`,
transition: 'none',
borderColor: `rgba(0, 0, 0, 0.3)`,
textShadow: `0 -1px 0 rgba(0, 0, 0, 0.2)`,
'&:hover, &:active': {
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.15) 100%)`,
},
'&:active': {
boxShadow: `inset 0 1px 3px rgba(0, 0, 0, 0.15)`,
},
}),
buttonColorDefault: css({
textShadow: 'none',
color: cssVar.colorText,
borderBottomColor: 'rgba(0, 0, 0, 0.5)',
}),
popupBox: css({
borderRadius: cssVar.borderRadiusLG,
backgroundColor: cssVar.colorBgContainer,
ul: {
paddingInline: 0,
},
}),
dropdownItem: css({
borderRadius: 0,
transition: 'none',
paddingBlock: cssVar.paddingXXS,
paddingInline: cssVar.padding,
'&:hover, &:active, &:focus': {
backgroundImage: `linear-gradient(to bottom, ${cssVar.colorPrimaryHover}, ${cssVar.colorPrimary})`,
color: cssVar.colorTextLightSolid,
},
}),
selectPopupRoot: css({
paddingInline: 0,
}),
switchRoot: css({
boxShadow: `inset 0 1px 3px rgba(0, 0, 0, 0.4)`,
}),
progressTrack: css({
backgroundImage: `linear-gradient(to bottom, ${cssVar.colorPrimaryHover}, ${cssVar.colorPrimary})`,
borderRadius: cssVar.borderRadiusSM,
}),
progressRail: css({
borderRadius: cssVar.borderRadiusSM,
}),
};
});
const useBootstrapTheme: UseTheme = () => {
const { styles } = useStyles();
return useMemo<ConfigProviderProps>(
() => ({
theme: {
algorithm: theme.defaultAlgorithm,
token: {
borderRadius: 4,
borderRadiusLG: 6,
colorInfo: '#3a87ad',
},
components: {
Tooltip: {
fontSize: 12,
},
Checkbox: {
colorBorder: '#666',
borderRadius: 2,
algorithm: true,
},
Radio: {
colorBorder: '#666',
borderRadius: 2,
algorithm: true,
},
},
},
wave: {
showEffect: () => {},
},
modal: {
classNames: {
container: clsx(styles.boxBorder, styles.modalContainer),
header: styles.modalHeader,
body: styles.modalBody,
footer: styles.modalFooter,
},
},
button: {
classNames: ({ props }) => ({
root: clsx(styles.buttonRoot, props.color === 'default' && styles.buttonColorDefault),
}),
},
alert: {
className: styles.alertRoot,
},
colorPicker: {
classNames: {
root: styles.boxBorder,
popup: {
root: clsx(styles.boxBorder, styles.popupBox),
},
},
arrow: false,
},
checkbox: {
classNames: {},
},
dropdown: {
classNames: {
root: clsx(styles.boxBorder, styles.popupBox),
item: styles.dropdownItem,
},
},
select: {
classNames: {
root: styles.boxBorder,
popup: {
root: clsx(styles.boxBorder, styles.selectPopupRoot),
listItem: styles.dropdownItem,
},
},
},
switch: {
classNames: {
root: styles.switchRoot,
},
},
progress: {
classNames: {
track: styles.progressTrack,
rail: styles.progressRail,
},
styles: {
rail: {
height: 20,
},
track: { height: 20 },
},
},
}),
[],
);
};
export default useBootstrapTheme;

View File

@@ -1,104 +0,0 @@
import { useMemo } from 'react';
import { theme } from 'antd';
import type { ConfigProviderProps } from 'antd';
import { createStyles } from 'antd-style';
import type { UseTheme } from '.';
const useStyles = createStyles(({ css, cssVar }) => {
const sharedBorder = {
border: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorBorder}`,
};
return {
sharedBorder,
progressTrack: css({
...sharedBorder,
marginInlineStart: `calc(-1 * ${cssVar.lineWidth})`,
marginBlockStart: `calc(-1 * ${cssVar.lineWidth})`,
}),
};
});
const useCartoonTheme: UseTheme = () => {
const { styles } = useStyles();
return useMemo<ConfigProviderProps>(
() => ({
theme: {
algorithm: theme.defaultAlgorithm,
token: {
colorText: '#51463B',
colorPrimary: '#225555',
colorError: '#DA8787',
colorInfo: '#9CD3D3',
colorInfoBorder: '#225555',
colorBorder: '#225555',
colorBorderSecondary: '#225555',
lineWidth: 2,
lineWidthBold: 2,
borderRadius: 18,
borderRadiusLG: 18,
borderRadiusSM: 18,
controlHeightSM: 28,
controlHeight: 36,
colorBgBase: '#FAFAEE',
},
components: {
Button: {
primaryShadow: 'none',
dangerShadow: 'none',
defaultShadow: 'none',
},
Modal: {
boxShadow: 'none',
},
Card: {
colorBgContainer: '#BBAA99',
},
Tooltip: {
borderRadius: 6,
colorBorder: '#225555',
algorithm: true,
},
Select: {
optionSelectedBg: '#CBC4AF',
},
},
},
// app: {
// className: styles.app,
// },
modal: {
classNames: {
container: styles.sharedBorder,
},
},
colorPicker: {
arrow: false,
},
popover: {
classNames: {
container: styles.sharedBorder,
},
},
progress: {
classNames: {
rail: styles.sharedBorder,
track: styles.progressTrack,
},
styles: {
rail: {
height: 16,
},
track: {
height: 16,
},
},
},
}),
[],
);
};
export default useCartoonTheme;

View File

@@ -1,157 +0,0 @@
import { useMemo } from 'react';
import { theme } from 'antd';
import type { ConfigProviderProps } from 'antd';
import { createStyles } from 'antd-style';
import clsx from 'clsx';
import type { UseTheme } from '.';
const useStyles = createStyles(({ css, cssVar }) => {
const lightBorder = {
border: `${cssVar.lineWidth} solid ${cssVar.colorPrimary}`,
boxShadow: `0 0 3px ${cssVar.colorPrimary}, inset 0 0 10px ${cssVar.colorPrimary}`,
};
return {
lightBorder,
app: css({
textShadow: `0 0 5px color-mix(in srgb, currentColor 50%, transparent)`,
}),
modalContainer: css({
...lightBorder,
padding: 0,
}),
modalHeader: css({
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
margin: 0,
position: 'relative',
'&:after': {
...lightBorder,
content: '""',
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
border: 0,
height: cssVar.lineWidth,
background: cssVar.colorPrimary,
},
}),
modalBody: css({
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
}),
modalFooter: css({
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
}),
buttonRoot: css({
...lightBorder,
border: undefined,
borderWidth: cssVar.lineWidth,
borderColor: cssVar.colorPrimary,
}),
buttonRootSolid: css({
color: cssVar.colorBgContainer,
border: 'none',
fontWeight: 'bolder',
}),
buttonRootSolidDanger: css({
boxShadow: `0 0 5px ${cssVar.colorError}`,
}),
colorPickerBody: css({
pointerEvents: 'none',
}),
tooltipRoot: css({
padding: cssVar.padding,
}),
tooltipContainer: css({
...lightBorder,
color: cssVar.colorPrimary,
}),
progressTrack: css({
backgroundColor: cssVar.colorPrimary,
}),
};
});
const useGeekTheme: UseTheme = () => {
const { styles } = useStyles();
return useMemo<ConfigProviderProps>(
() => ({
theme: {
algorithm: theme.darkAlgorithm,
token: {
borderRadius: 0,
lineWidth: 2,
colorPrimary: '#39ff14',
colorText: '#39ff14',
controlHeightSM: 26,
controlHeight: 34,
},
},
app: {
className: styles.app,
},
modal: {
classNames: {
container: styles.modalContainer,
header: styles.modalHeader,
body: styles.modalBody,
footer: styles.modalFooter,
},
},
button: {
classNames: ({ props }) => ({
root: clsx(
styles.buttonRoot,
props.variant === 'solid' && styles.buttonRootSolid,
props.variant === 'solid' && props.danger && styles.buttonRootSolidDanger,
),
}),
},
alert: {
className: styles.lightBorder,
},
colorPicker: {
classNames: {
root: styles.lightBorder,
body: styles.colorPickerBody,
},
arrow: false,
},
select: {
classNames: {
root: styles.lightBorder,
},
},
input: {
classNames: {
root: styles.lightBorder,
},
},
inputNumber: {
classNames: {
root: styles.lightBorder,
},
},
tooltip: {
arrow: false,
classNames: {
root: styles.tooltipRoot,
container: styles.tooltipContainer,
},
},
progress: {
classNames: {
track: styles.progressTrack,
},
},
}),
[],
);
};
export default useGeekTheme;

View File

@@ -1,157 +0,0 @@
import { useMemo } from 'react';
import { theme } from 'antd';
import type { ConfigProviderProps } from 'antd';
import { createStyles } from 'antd-style';
import clsx from 'clsx';
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)`,
`inset 0 5px 2px rgba(255, 255, 255, 0.2)`,
].join(','),
};
const glassBox = {
...glassBorder,
background: `color-mix(in srgb, ${cssVar.colorBgContainer} 15%, transparent)`,
backdropFilter: 'blur(12px)',
};
return {
glassBorder,
glassBox,
notBackdropFilter: css({
backdropFilter: 'none',
}),
app: css({
textShadow: '0 1px rgba(0,0,0,0.1)',
}),
cardRoot: css({
...glassBox,
backgroundColor: `color-mix(in srgb, ${cssVar.colorBgContainer} 40%, transparent)`,
}),
modalContainer: css({
...glassBox,
backdropFilter: 'none',
}),
buttonRoot: css({
...glassBorder,
}),
buttonRootDefaultColor: css({
background: 'transparent',
color: cssVar.colorText,
'&:hover': {
background: 'rgba(255,255,255,0.2)',
color: `color-mix(in srgb, ${cssVar.colorText} 90%, transparent)`,
},
'&:active': {
background: 'rgba(255,255,255,0.1)',
color: `color-mix(in srgb, ${cssVar.colorText} 80%, transparent)`,
},
}),
dropdownRoot: css({
...glassBox,
borderRadius: cssVar.borderRadiusLG,
ul: {
background: 'transparent',
},
}),
switchRoot: css({ ...glassBorder, border: 'none' }),
};
});
const useGlassTheme: UseTheme = () => {
const { styles } = useStyles();
return useMemo<ConfigProviderProps>(
() => ({
theme: {
algorithm: theme.defaultAlgorithm,
token: {
borderRadius: 12,
borderRadiusLG: 12,
borderRadiusSM: 12,
borderRadiusXS: 12,
motionDurationSlow: '0.2s',
motionDurationMid: '0.1s',
motionDurationFast: '0.05s',
},
},
app: {
className: styles.app,
},
card: {
classNames: {
root: styles.cardRoot,
},
},
modal: {
classNames: {
container: styles.modalContainer,
},
},
button: {
classNames: ({ props }) => ({
root: clsx(
styles.buttonRoot,
(props.variant !== 'solid' || props.color === 'default' || props.type === 'default') &&
styles.buttonRootDefaultColor,
),
}),
},
alert: {
className: clsx(styles.glassBox, styles.notBackdropFilter),
},
colorPicker: {
arrow: false,
},
dropdown: {
classNames: {
root: styles.dropdownRoot,
},
},
select: {
classNames: {
root: clsx(styles.glassBox, styles.notBackdropFilter),
popup: {
root: styles.glassBox,
},
},
},
popover: {
classNames: {
container: styles.glassBox,
},
},
switch: {
classNames: {
root: styles.switchRoot,
},
},
progress: {
classNames: {
track: styles.glassBorder,
},
styles: {
track: {
height: 12,
},
rail: {
height: 12,
},
},
},
}),
[],
);
};
export default useGlassTheme;

View File

@@ -1,184 +0,0 @@
import { useMemo } from 'react';
import { theme } from 'antd';
import type { ConfigProviderProps } from 'antd';
import { createStyles } from 'antd-style';
import type { UseTheme } from '.';
const useStyles = createStyles(({ css, cssVar }) => {
const illustrationBorder = {
border: `${cssVar.lineWidth} solid ${cssVar.colorBorder}`,
};
const illustrationBox = {
...illustrationBorder,
boxShadow: `4px 4px 0 ${cssVar.colorBorder}`,
};
return {
illustrationBorder,
illustrationBox,
buttonRoot: css({
...illustrationBox,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.5px',
}),
modalContainer: css({
...illustrationBox,
}),
tooltipRoot: css({
padding: cssVar.padding,
}),
popupBox: css({
...illustrationBox,
borderRadius: cssVar.borderRadiusLG,
backgroundColor: cssVar.colorBgContainer,
}),
progressRail: css({
border: `${cssVar.lineWidth} solid ${cssVar.colorBorder}`,
boxShadow: `2px 2px 0 ${cssVar.colorBorder}`,
}),
progressTrack: css({
border: 'none',
}),
inputNumberActions: css({
width: 12,
}),
};
});
const useIllustrationTheme: UseTheme = () => {
const { styles } = useStyles();
return useMemo<ConfigProviderProps>(
() => ({
theme: {
algorithm: theme.defaultAlgorithm,
token: {
colorText: '#2C2C2C',
colorPrimary: '#52C41A',
colorSuccess: '#51CF66',
colorWarning: '#FFD93D',
colorError: '#FA5252',
colorInfo: '#4DABF7',
colorBorder: '#2C2C2C',
colorBorderSecondary: '#2C2C2C',
lineWidth: 3,
lineWidthBold: 3,
borderRadius: 12,
borderRadiusLG: 16,
borderRadiusSM: 8,
controlHeight: 40,
controlHeightSM: 34,
controlHeightLG: 48,
fontSize: 15,
fontWeightStrong: 600,
colorBgBase: '#FFF9F0',
colorBgContainer: '#FFFFFF',
},
components: {
Button: {
primaryShadow: 'none',
dangerShadow: 'none',
defaultShadow: 'none',
fontWeight: 600,
},
Modal: {
boxShadow: 'none',
},
Card: {
boxShadow: '4px 4px 0 #2C2C2C',
colorBgContainer: '#FFF0F6',
},
Tooltip: {
colorBorder: '#2C2C2C',
colorBgSpotlight: 'rgba(100, 100, 100, 0.95)',
borderRadius: 8,
},
Select: {
optionSelectedBg: 'transparent',
},
Slider: {
dotBorderColor: '#237804',
dotActiveBorderColor: '#237804',
colorPrimaryBorder: '#237804',
colorPrimaryBorderHover: '#237804',
},
},
},
button: {
classNames: {
root: styles.buttonRoot,
},
},
modal: {
classNames: {
container: styles.modalContainer,
},
},
alert: {
className: styles.illustrationBorder,
},
colorPicker: {
arrow: false,
classNames: {
root: styles.illustrationBox,
},
},
popover: {
classNames: {
container: styles.illustrationBox,
},
},
tooltip: {
arrow: false,
classNames: {
root: styles.tooltipRoot,
container: styles.illustrationBox,
},
},
dropdown: {
classNames: {
root: styles.popupBox,
},
},
select: {
classNames: {
root: styles.illustrationBox,
popup: {
root: styles.popupBox,
},
},
},
input: {
classNames: {
root: styles.illustrationBox,
},
},
inputNumber: {
classNames: {
root: styles.illustrationBox,
actions: styles.inputNumberActions,
},
},
progress: {
classNames: {
rail: styles.progressRail,
track: styles.progressTrack,
},
styles: {
rail: {
height: 16,
},
track: {
height: 10,
},
},
},
}),
[],
);
};
export default useIllustrationTheme;

View File

@@ -1,132 +0,0 @@
import React from 'react';
import type { ConfigProviderProps } from 'antd';
import { theme } from 'antd';
import useLocale from '../../../../../hooks/useLocale';
import useBootstrapTheme from './bootstrapTheme';
import useCartoonTheme from './cartoonTheme';
import useGeekTheme from './geekTheme';
import useGlassTheme from './glassTheme';
import useIllustrationTheme from './illustrationTheme';
import useMuiTheme from './muiTheme';
import useShadcnTheme from './shadcnTheme';
type PreviewThemeConfig = {
name: string;
key?: string;
props?: ConfigProviderProps;
bgImg?: string;
bgImgDark?: true;
};
const locales = {
cn: {
default: '默认风格',
dark: '暗黑风格',
geek: '极客风格',
glass: '玻璃风格',
mui: '类 MUI 风格',
shadcn: '类 shadcn 风格',
bootstrap: '类 Bootstrap 拟物化风格',
cartoon: '卡通风格',
illustration: '插画风格',
},
en: {
default: 'Default Style',
dark: 'Dark Style',
geek: 'Geek Style',
glass: 'Glass Style',
mui: 'MUI-like Style',
shadcn: 'shadcn-like Style',
bootstrap: 'Bootstrap Skeuomorphism',
cartoon: 'Cartoon Style',
illustration: 'Illustration Style',
},
};
export type UseTheme = () => ConfigProviderProps;
export default function usePreviewThemes() {
const [locale] = useLocale(locales);
const cartoonTheme = useCartoonTheme();
const illustrationTheme = useIllustrationTheme();
const geekTheme = useGeekTheme();
const glassTheme = useGlassTheme();
const muiTheme = useMuiTheme();
const shadcnTheme = useShadcnTheme();
const bootstrapTheme = useBootstrapTheme();
return React.useMemo<PreviewThemeConfig[]>(() => {
return [
{
name: locale.default,
key: 'light',
bgImg:
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*T8IlRaNez08AAAAARwAAAAgAegCCAQ/original',
props: {
theme: {
algorithm: theme.defaultAlgorithm,
},
},
},
{
name: locale.dark,
key: 'dark',
dark: true,
bgImg:
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*ETkNSJ-oUGwAAAAAQ_AAAAgAegCCAQ/original',
bgImgDark: true,
props: {
theme: {
algorithm: theme.darkAlgorithm,
},
},
},
{
name: locale.mui,
bgImg:
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*IFkZRpIKEEkAAAAAQzAAAAgAegCCAQ/original',
props: muiTheme,
},
{
name: locale.shadcn,
bgImg:
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*56tPQbwgFyEAAAAARuAAAAgAegCCAQ/original',
props: shadcnTheme,
},
{
name: locale.cartoon,
bgImg:
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*tgpBT7vYIUsAAAAAQ-AAAAgAegCCAQ/original',
props: cartoonTheme,
},
{
name: locale.illustration,
bgImg:
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*HuVGQKqOER0AAAAARsAAAAgAegCCAQ/original',
props: illustrationTheme,
},
{
name: locale.bootstrap,
bgImg:
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*ZrLfQIO34x4AAAAAS4AAAAgAegCCAQ/original',
props: bootstrapTheme,
},
{
name: locale.glass,
bgImg:
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*PbKXQLie7OAAAAAARTAAAAgAegCCAQ/original',
bgImgDark: true,
props: glassTheme,
},
{
name: locale.geek,
bgImg:
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*fzA2T4ms154AAAAARtAAAAgAegCCAQ/original',
bgImgDark: true,
props: geekTheme,
},
];
}, [locale]);
}

View File

@@ -1,288 +0,0 @@
import { useMemo } from 'react';
import raf from '@rc-component/util/lib/raf';
import { theme } from 'antd';
import type { ConfigProviderProps, GetProp } from 'antd';
import { createStyles } from 'antd-style';
import clsx from 'clsx';
import type { UseTheme } from '.';
type WaveConfig = GetProp<ConfigProviderProps, 'wave'>;
// Prepare effect holder
const createHolder = (node: HTMLElement) => {
const { borderWidth } = getComputedStyle(node);
const borderWidthNum = Number.parseInt(borderWidth, 10);
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.inset = `-${borderWidthNum}px`;
div.style.borderRadius = 'inherit';
div.style.background = 'transparent';
div.style.zIndex = '999';
div.style.pointerEvents = 'none';
div.style.overflow = 'hidden';
node.appendChild(div);
return div;
};
const createDot = (holder: HTMLElement, color: string, left: number, top: number, size = 0) => {
const dot = document.createElement('div');
dot.style.position = 'absolute';
dot.style.left = `${left}px`;
dot.style.top = `${top}px`;
dot.style.width = `${size}px`;
dot.style.height = `${size}px`;
dot.style.borderRadius = '50%';
dot.style.background = color;
dot.style.transform = 'translate3d(-50%, -50%, 0)';
dot.style.transition = 'all 1s ease-out';
holder.appendChild(dot);
return dot;
};
// Inset Effect
const showInsetEffect: WaveConfig['showEffect'] = (node, { event, component }) => {
if (component !== 'Button') {
return;
}
const holder = createHolder(node);
const rect = holder.getBoundingClientRect();
const left = event.clientX - rect.left;
const top = event.clientY - rect.top;
const dot = createDot(holder, 'rgba(255, 255, 255, 0.65)', left, top);
// Motion
raf(() => {
dot.ontransitionend = () => {
holder.remove();
};
dot.style.width = '200px';
dot.style.height = '200px';
dot.style.opacity = '0';
});
};
const useStyles = createStyles(({ css }) => {
return {
buttonPrimary: css({
backgroundColor: '#1976d2',
color: '#ffffff',
border: 'none',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.02857em',
boxShadow:
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
buttonDefault: css({
backgroundColor: '#ffffff',
color: 'rgba(0, 0, 0, 0.87)',
border: '1px solid rgba(0, 0, 0, 0.23)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.02857em',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
buttonDanger: css({
backgroundColor: '#d32f2f',
color: '#ffffff',
border: 'none',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.02857em',
boxShadow:
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
}),
inputRoot: css({
borderColor: 'rgba(0, 0, 0, 0.23)',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
inputElement: css({
color: 'rgba(0, 0, 0, 0.87)',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
}),
inputError: css({
borderColor: '#d32f2f',
}),
selectRoot: css({
borderColor: 'rgba(0, 0, 0, 0.23)',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
}),
selectPopup: css({
borderRadius: '4px',
boxShadow:
'0px 5px 5px -3px rgba(0,0,0,0.2), 0px 8px 10px 1px rgba(0,0,0,0.14), 0px 3px 14px 2px rgba(0,0,0,0.12)',
}),
};
});
const useMuiTheme: UseTheme = () => {
const { styles } = useStyles();
return useMemo<ConfigProviderProps>(
() => ({
theme: {
algorithm: theme.defaultAlgorithm,
token: {
colorPrimary: '#1976d2',
colorSuccess: '#2e7d32',
colorWarning: '#ed6c02',
colorError: '#d32f2f',
colorInfo: '#0288d1',
colorTextBase: '#212121',
colorBgBase: '#fafafa',
colorPrimaryBg: '#e3f2fd',
colorPrimaryBgHover: '#bbdefb',
colorPrimaryBorder: '#90caf9',
colorPrimaryBorderHover: '#64b5f6',
colorPrimaryHover: '#42a5f5',
colorPrimaryActive: '#1565c0',
colorPrimaryText: '#1976d2',
colorPrimaryTextHover: '#42a5f5',
colorPrimaryTextActive: '#1565c0',
colorSuccessBg: '#e8f5e9',
colorSuccessBgHover: '#c8e6c9',
colorSuccessBorder: '#a5d6a7',
colorSuccessBorderHover: '#81c784',
colorSuccessHover: '#4caf50',
colorSuccessActive: '#1b5e20',
colorSuccessText: '#2e7d32',
colorSuccessTextHover: '#4caf50',
colorSuccessTextActive: '#1b5e20',
colorWarningBg: '#fff3e0',
colorWarningBgHover: '#ffe0b2',
colorWarningBorder: '#ffcc02',
colorWarningBorderHover: '#ffb74d',
colorWarningHover: '#ff9800',
colorWarningActive: '#e65100',
colorWarningText: '#ed6c02',
colorWarningTextHover: '#ff9800',
colorWarningTextActive: '#e65100',
colorErrorBg: '#ffebee',
colorErrorBgHover: '#ffcdd2',
colorErrorBorder: '#ef9a9a',
colorErrorBorderHover: '#e57373',
colorErrorHover: '#ef5350',
colorErrorActive: '#c62828',
colorErrorText: '#d32f2f',
colorErrorTextHover: '#ef5350',
colorErrorTextActive: '#c62828',
colorInfoBg: '#e1f5fe',
colorInfoBgHover: '#b3e5fc',
colorInfoBorder: '#81d4fa',
colorInfoBorderHover: '#4fc3f7',
colorInfoHover: '#03a9f4',
colorInfoActive: '#01579b',
colorInfoText: '#0288d1',
colorInfoTextHover: '#03a9f4',
colorInfoTextActive: '#01579b',
colorText: 'rgba(33, 33, 33, 0.87)',
colorTextSecondary: 'rgba(33, 33, 33, 0.6)',
colorTextTertiary: 'rgba(33, 33, 33, 0.38)',
colorTextQuaternary: 'rgba(33, 33, 33, 0.26)',
colorTextDisabled: 'rgba(33, 33, 33, 0.38)',
colorBgContainer: '#ffffff',
colorBgElevated: '#ffffff',
colorBgLayout: '#f5f5f5',
colorBgSpotlight: 'rgba(33, 33, 33, 0.85)',
colorBgMask: 'rgba(33, 33, 33, 0.5)',
colorBorder: '#e0e0e0',
colorBorderSecondary: '#eeeeee',
borderRadius: 4,
borderRadiusXS: 1,
borderRadiusSM: 2,
borderRadiusLG: 6,
padding: 16,
paddingSM: 8,
paddingLG: 24,
margin: 16,
marginSM: 8,
marginLG: 24,
boxShadow:
'0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12)',
boxShadowSecondary:
'0px 3px 3px -2px rgba(0,0,0,0.2),0px 3px 4px 0px rgba(0,0,0,0.14),0px 1px 8px 0px rgba(0,0,0,0.12)',
},
components: {
Button: {
primaryShadow:
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
defaultShadow:
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
dangerShadow:
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
fontWeight: 500,
defaultBorderColor: 'rgba(0, 0, 0, 0.23)',
defaultColor: 'rgba(0, 0, 0, 0.87)',
defaultBg: '#ffffff',
defaultHoverBg: 'rgba(25, 118, 210, 0.04)',
defaultHoverBorderColor: 'rgba(0, 0, 0, 0.23)',
paddingInline: 16,
paddingBlock: 6,
contentFontSize: 14,
borderRadius: 4,
},
Alert: {
borderRadiusLG: 4,
},
Modal: {
borderRadiusLG: 4,
},
Progress: {
defaultColor: '#1976d2',
remainingColor: 'rgba(25, 118, 210, 0.12)',
},
Steps: {
iconSize: 24,
},
Checkbox: {
borderRadiusSM: 2,
},
Slider: {
trackBg: 'rgba(25, 118, 210, 0.26)',
trackHoverBg: 'rgba(25, 118, 210, 0.38)',
handleSize: 20,
handleSizeHover: 20,
railSize: 4,
},
ColorPicker: {
borderRadius: 4,
},
},
},
wave: {
showEffect: showInsetEffect,
},
button: {
classNames: ({ props }) => ({
root: clsx(
props.type === 'primary' && styles.buttonPrimary,
props.type === 'default' && styles.buttonDefault,
props.danger && styles.buttonDanger,
),
}),
},
input: {
classNames: ({ props }) => ({
root: clsx(styles.inputRoot, props.status === 'error' && styles.inputError),
input: styles.inputElement,
}),
},
select: {
classNames: {
root: styles.selectRoot,
},
},
}),
[styles],
);
};
export default useMuiTheme;

View File

@@ -1,222 +0,0 @@
import { useMemo } from 'react';
import { theme } from 'antd';
import type { ConfigProviderProps } from 'antd';
import { createStyles } from 'antd-style';
import clsx from 'clsx';
import type { UseTheme } from '.';
const useStyles = createStyles(({ css }) => {
return {
buttonPrimary: css({
backgroundColor: '#18181b',
color: '#ffffff',
border: '1px solid #18181b',
fontWeight: 500,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
buttonDefault: css({
backgroundColor: '#ffffff',
color: '#18181b',
border: '1px solid #e4e4e7',
fontWeight: 500,
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
buttonDanger: css({
backgroundColor: '#dc2626',
color: '#ffffff',
border: '1px solid #dc2626',
fontWeight: 500,
}),
inputRoot: css({
borderColor: '#e4e4e7',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}),
inputElement: css({
color: '#18181b',
}),
inputError: css({
borderColor: '#dc2626',
}),
selectRoot: css({
borderColor: '#e4e4e7',
}),
selectPopup: css({
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
}),
};
});
const useShadcnTheme: UseTheme = () => {
const { styles } = useStyles();
return useMemo<ConfigProviderProps>(
() => ({
theme: {
algorithm: theme.defaultAlgorithm,
token: {
colorPrimary: '#262626',
colorSuccess: '#22c55e',
colorWarning: '#f97316',
colorError: '#ef4444',
colorInfo: '#262626',
colorTextBase: '#262626',
colorBgBase: '#ffffff',
colorPrimaryBg: '#f5f5f5',
colorPrimaryBgHover: '#e5e5e5',
colorPrimaryBorder: '#d4d4d4',
colorPrimaryBorderHover: '#a3a3a3',
colorPrimaryHover: '#404040',
colorPrimaryActive: '#171717',
colorPrimaryText: '#262626',
colorPrimaryTextHover: '#404040',
colorPrimaryTextActive: '#171717',
colorSuccessBg: '#f0fdf4',
colorSuccessBgHover: '#dcfce7',
colorSuccessBorder: '#bbf7d0',
colorSuccessBorderHover: '#86efac',
colorSuccessHover: '#16a34a',
colorSuccessActive: '#15803d',
colorSuccessText: '#16a34a',
colorSuccessTextHover: '#16a34a',
colorSuccessTextActive: '#15803d',
colorWarningBg: '#fff7ed',
colorWarningBgHover: '#fed7aa',
colorWarningBorder: '#fdba74',
colorWarningBorderHover: '#fb923c',
colorWarningHover: '#ea580c',
colorWarningActive: '#c2410c',
colorWarningText: '#ea580c',
colorWarningTextHover: '#ea580c',
colorWarningTextActive: '#c2410c',
colorErrorBg: '#fef2f2',
colorErrorBgHover: '#fecaca',
colorErrorBorder: '#fca5a5',
colorErrorBorderHover: '#f87171',
colorErrorHover: '#dc2626',
colorErrorActive: '#b91c1c',
colorErrorText: '#dc2626',
colorErrorTextHover: '#dc2626',
colorErrorTextActive: '#b91c1c',
colorInfoBg: '#f5f5f5',
colorInfoBgHover: '#e5e5e5',
colorInfoBorder: '#d4d4d4',
colorInfoBorderHover: '#a3a3a3',
colorInfoHover: '#404040',
colorInfoActive: '#171717',
colorInfoText: '#262626',
colorInfoTextHover: '#404040',
colorInfoTextActive: '#171717',
colorText: '#262626',
colorTextSecondary: '#525252',
colorTextTertiary: '#737373',
colorTextQuaternary: '#a3a3a3',
colorTextDisabled: '#a3a3a3',
colorBgContainer: '#ffffff',
colorBgElevated: '#ffffff',
colorBgLayout: '#fafafa',
colorBgSpotlight: 'rgba(38, 38, 38, 0.85)',
colorBgMask: 'rgba(38, 38, 38, 0.45)',
colorBorder: '#e5e5e5',
colorBorderSecondary: '#f5f5f5',
borderRadius: 10,
borderRadiusXS: 2,
borderRadiusSM: 6,
borderRadiusLG: 14,
padding: 16,
paddingSM: 12,
paddingLG: 24,
margin: 16,
marginSM: 12,
marginLG: 24,
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
boxShadowSecondary:
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
},
components: {
Button: {
primaryShadow: 'none',
defaultShadow: 'none',
dangerShadow: 'none',
defaultBorderColor: '#e4e4e7',
defaultColor: '#18181b',
defaultBg: '#ffffff',
defaultHoverBg: '#f4f4f5',
defaultHoverBorderColor: '#d4d4d8',
defaultHoverColor: '#18181b',
defaultActiveBg: '#e4e4e7',
defaultActiveBorderColor: '#d4d4d8',
borderRadius: 6,
},
Input: {
activeShadow: 'none',
hoverBorderColor: '#a1a1aa',
activeBorderColor: '#18181b',
borderRadius: 6,
},
Select: {
optionSelectedBg: '#f4f4f5',
optionActiveBg: '#fafafa',
optionSelectedFontWeight: 500,
borderRadius: 6,
},
Alert: {
borderRadiusLG: 8,
},
Modal: {
borderRadiusLG: 12,
},
Progress: {
defaultColor: '#18181b',
remainingColor: '#f4f4f5',
},
Steps: {
iconSize: 32,
},
Switch: {
trackHeight: 24,
trackMinWidth: 44,
innerMinMargin: 4,
innerMaxMargin: 24,
},
Checkbox: {
borderRadiusSM: 4,
},
Slider: {
trackBg: '#f4f4f5',
trackHoverBg: '#e4e4e7',
handleSize: 18,
handleSizeHover: 20,
railSize: 6,
},
ColorPicker: {
borderRadius: 6,
},
},
},
button: {
classNames: ({ props }) => ({
root: clsx(
props.type === 'primary' && styles.buttonPrimary,
props.type === 'default' && styles.buttonDefault,
props.danger && styles.buttonDanger,
),
}),
},
input: {
classNames: ({ props }) => ({
root: clsx(styles.inputRoot, props.status === 'error' && styles.inputError),
input: styles.inputElement,
}),
},
select: {
classNames: {
root: styles.selectRoot,
},
},
}),
[styles],
);
};
export default useShadcnTheme;

View File

@@ -1,3 +1,4 @@
/* eslint-disable compat/compat */
import { css } from 'antd-style';
import useSWR from 'swr';

View File

@@ -1,19 +1,18 @@
import React, { Suspense } from 'react';
import { theme } from 'antd';
import { createStaticStyles } from 'antd-style';
import { ConfigProvider, theme } from 'antd';
import { createStyles, css } from 'antd-style';
import useLocale from '../../hooks/useLocale';
import { DarkContext } from './../../hooks/useDark';
import BannerRecommends from './components/BannerRecommends';
import Group from './components/Group';
import PreviewBanner from './components/PreviewBanner';
import ThemePreview from './components/ThemePreview';
const ComponentsList = React.lazy(() => import('./components/ComponentsList'));
const DesignFramework = React.lazy(() => import('./components/DesignFramework'));
// const Theme = React.lazy(() => import('./components/Theme'));
const Theme = React.lazy(() => import('./components/Theme'));
const classNames = createStaticStyles(({ css }) => ({
const useStyle = createStyles(() => ({
image: css`
position: absolute;
inset-inline-start: 0;
@@ -39,6 +38,7 @@ const locales = {
const Homepage: React.FC = () => {
const [locale] = useLocale(locales);
const { styles } = useStyle();
const { token } = theme.useToken();
const isDark = React.use(DarkContext);
@@ -49,96 +49,48 @@ const Homepage: React.FC = () => {
<BannerRecommends />
</PreviewBanner>
{/* 定制主题 */}
{/* <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
<Suspense fallback={null}>
<Theme />
</Suspense>
</ConfigProvider> */}
<ThemePreview />
<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
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>
{/* 设计语言 */}
<Group
title={locale.designTitle}
description={locale.designDesc}
background={isDark ? '#393F4A' : '#F5F8FF'}
decoration={
<img
draggable={false}
className={styles.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>
);
// return (
// <section>
// <PreviewBanner>
// <BannerRecommends />
// </PreviewBanner>
// <div>
// {/* 定制主题 */}
// <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
// <Suspense fallback={null}>
// <Theme />
// </Suspense>
// </ConfigProvider>
// {/* 组件列表 */}
// <Group
// background={token.colorBgElevated}
// collapse
// title={locale.assetsTitle}
// description={locale.assetsDesc}
// id="design"
// >
// <Suspense fallback={null}>
// <ComponentsList />
// </Suspense>
// </Group>
// {/* 设计语言 */}
// <Group
// title={locale.designTitle}
// description={locale.designDesc}
// background={isDark ? '#393F4A' : '#F5F8FF'}
// decoration={
// <img
// draggable={false}
// className={classNames.image}
// src="https://gw.alipayobjects.com/zos/bmw-prod/ba37a413-28e6-4be4-b1c5-01be1a0ebb1c.svg"
// alt="bg"
// />
// }
// >
// <Suspense fallback={null}>
// <DesignFramework />
// </Suspense>
// </Group>
// </div>
// </section>
// );
};
export default Homepage;

View File

@@ -1,6 +1,6 @@
import React, { Suspense } from 'react';
import { App, Button, ConfigProvider, Skeleton, version } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { enUS, zhCN } from 'antd-token-previewer';
import type { ThemeConfig } from 'antd/es/config-provider/context';
import { Helmet } from 'dumi';
@@ -9,7 +9,7 @@ import useLocale from '../../hooks/useLocale';
const ThemeEditor = React.lazy(() => import('antd-token-previewer/lib/ThemeEditor'));
const classNames = createStaticStyles(({ css }) => ({
const useStyle = createStyles(({ css }) => ({
editor: css`
svg,
img {
@@ -49,6 +49,7 @@ const ANT_DESIGN_V5_THEME_EDITOR_THEME = `ant-design-v${antdMajor}-theme-editor-
const CustomTheme: React.FC = () => {
const { message } = App.useApp();
const [locale, lang] = useLocale(locales);
const { styles } = useStyle();
const [theme, setTheme] = React.useState<ThemeConfig>(() => {
try {
@@ -77,7 +78,7 @@ const CustomTheme: React.FC = () => {
hideAdvancedSwitcher
theme={{ name: 'Custom Theme', key: 'test', config: theme }}
style={{ height: 'calc(100vh - 64px)' }}
className={classNames.editor}
className={styles.editor}
onThemeChange={(newTheme) => {
setTheme(newTheme.config);
}}

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { SoundOutlined } from '@ant-design/icons';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
const styles = createStaticStyles(({ css, cssVar }) => {
const useStyle = createStyles(({ css, cssVar }) => {
return {
playBtn: css`
display: inline-flex;
@@ -30,6 +30,7 @@ interface AudioProps {
}
const AudioControl: React.FC<React.PropsWithChildren<AudioProps>> = ({ id, children }) => {
const { styles } = useStyle();
const onClick: React.MouseEventHandler<HTMLAnchorElement> = () => {
const audio = document.querySelector<HTMLAudioElement>(`#${id}`);
audio?.play();

View File

@@ -3,7 +3,6 @@ import {
BugOutlined,
CompassOutlined,
EditOutlined,
FileTextOutlined,
GithubOutlined,
HistoryOutlined,
IssuesCloseOutlined,
@@ -33,7 +32,6 @@ const locales = {
version: '版本',
issueNew: '提交问题',
issueOpen: '待解决',
copyError: '复制失败',
},
en: {
import: 'Import',
@@ -47,7 +45,6 @@ const locales = {
version: 'Version',
issueNew: 'Issue',
issueOpen: 'Open issues',
copyError: 'Copy failed',
},
};
@@ -131,22 +128,21 @@ const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
};
// ======================== Source ========================
const [filledSource, abbrSource, llmsPath] = React.useMemo(() => {
const [filledSource, abbrSource] = React.useMemo(() => {
if (String(source) === 'true') {
const kebabComponent = kebabCase(component);
return [
`https://github.com/${repo}/blob/master/components/${kebabComponent}`,
`components/${kebabComponent}`,
`/components/${kebabComponent}${isZhCN ? '-cn' : ''}.md`,
];
}
if (typeof source !== 'string') {
return [null, null, null];
return [null, null];
}
return [source, source, null];
}, [component, repo, source, isZhCN]);
return [source, source];
}, [component, repo, source]);
return (
<Descriptions
@@ -214,15 +210,6 @@ const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
<span>{locale.design}</span>
</Link>
)}
<Typography.Link
className={styles.code}
href={llmsPath || ''}
target="_blank"
rel="noreferrer"
>
<FileTextOutlined className={styles.icon} />
<span>LLMs.md</span>
</Typography.Link>
<ComponentChangelog>
<Typography.Link className={styles.code}>
<HistoryOutlined className={styles.icon} />

View File

@@ -2,7 +2,7 @@ import React, { memo, useMemo, useRef, useState } from 'react';
import type { CSSProperties } from 'react';
import { SearchOutlined } from '@ant-design/icons';
import { Affix, Card, Col, Divider, Flex, Input, Row, Tag, Typography } from 'antd';
import { createStaticStyles, useTheme } from 'antd-style';
import { createStyles, useTheme } from 'antd-style';
import { useIntl, useLocation, useSidebarData } from 'dumi';
import debounce from 'lodash/debounce';
import scrollIntoView from 'scroll-into-view-if-needed';
@@ -12,7 +12,7 @@ import SiteContext from '../../slots/SiteContext';
import type { Component } from './ProComponentsList';
import proComponentsList from './ProComponentsList';
const styles = createStaticStyles(({ cssVar, css }) => ({
const useStyle = createStyles(({ cssVar, css }) => ({
componentsOverviewGroupTitle: css`
margin-bottom: ${cssVar.marginLG} !important;
`,
@@ -78,6 +78,7 @@ const reportSearch = debounce<(value: string) => void>((value) => {
const { Title } = Typography;
const Overview: React.FC = () => {
const { styles } = useStyle();
const { isDark } = React.use(SiteContext);
const data = useSidebarData();

View File

@@ -1,13 +1,13 @@
import * as React from 'react';
import { App } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { useIntl } from 'dumi';
import CopyableIcon from './CopyableIcon';
import type { CategoriesKeys } from './fields';
import type { ThemeType } from './IconSearch';
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ css, cssVar }) => ({
anticonsList: css`
margin: ${cssVar.margin} 0;
overflow: hidden;
@@ -36,6 +36,7 @@ interface CategoryProps {
const Category: React.FC<CategoryProps> = (props) => {
const { message } = App.useApp();
const { icons, title, newIcons, theme } = props;
const { styles } = useStyle();
const intl = useIntl();
const [justCopied, setJustCopied] = React.useState<string | null>(null);
const copyId = React.useRef<ReturnType<typeof setTimeout> | null>(null);

View File

@@ -2,7 +2,7 @@ import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import Icon, * as AntdIcons from '@ant-design/icons';
import { Affix, Empty, Input, Segmented } from 'antd';
import { createStaticStyles, useTheme } from 'antd-style';
import { createStyles, useTheme } from 'antd-style';
import type { SegmentedOptions } from 'antd/es/segmented';
import { useIntl } from 'dumi';
import debounce from 'lodash/debounce';
@@ -22,7 +22,7 @@ export enum ThemeType {
const allIcons: { [key: string]: any } = AntdIcons;
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ css, cssVar }) => ({
iconSearchAffix: css`
display: flex;
transition: all ${cssVar.motionDurationSlow};
@@ -39,6 +39,7 @@ const NEW_ICON_NAMES: ReadonlyArray<string> = [];
const IconSearch: React.FC = () => {
const intl = useIntl();
const { styles } = useStyle();
const [displayState, setDisplayState] = useState<IconSearchState>({
searchKey: '',
theme: ThemeType.Outlined,

View File

@@ -1,10 +1,10 @@
import React, { Suspense } from 'react';
import { Skeleton } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
const IconSearch = React.lazy(() => import('./IconSearch'));
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ css, cssVar }) => ({
searchWrapper: css`
display: flex;
gap: ${cssVar.padding};
@@ -34,6 +34,8 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
}));
const IconSearchFallback: React.FC = () => {
const { styles } = useStyle();
return (
<>
<div className={styles.searchWrapper}>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles, css } from 'antd-style';
import { clsx } from 'clsx';
interface IconProps {
@@ -7,7 +7,7 @@ interface IconProps {
style?: React.CSSProperties;
}
const classNames = createStaticStyles(({ css }) => ({
const useStyle = createStyles(() => ({
iconWrap: css`
display: inline-flex;
align-items: center;
@@ -19,8 +19,9 @@ const classNames = createStaticStyles(({ css }) => ({
const BunIcon: React.FC<IconProps> = (props) => {
const { className, style } = props;
const { styles } = useStyle();
return (
<span className={clsx(classNames.iconWrap, className)} style={style}>
<span className={clsx(styles.iconWrap, className)} style={style}>
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70" width="1em" height="1em">
<title>Bun Logo</title>
<path

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles, css } from 'antd-style';
import { clsx } from 'clsx';
interface IconProps {
@@ -7,7 +7,7 @@ interface IconProps {
style?: React.CSSProperties;
}
const classNames = createStaticStyles(({ css }) => ({
const useStyle = createStyles(() => ({
iconWrap: css`
display: inline-flex;
align-items: center;
@@ -19,8 +19,9 @@ const classNames = createStaticStyles(({ css }) => ({
const NpmIcon: React.FC<IconProps> = (props) => {
const { className, style } = props;
const { styles } = useStyle();
return (
<span className={clsx(classNames.iconWrap, className)} style={style}>
<span className={clsx(styles.iconWrap, className)} style={style}>
<svg
fill="#E53E3E"
focusable="false"

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles, css } from 'antd-style';
import { clsx } from 'clsx';
interface IconProps {
@@ -7,7 +7,7 @@ interface IconProps {
style?: React.CSSProperties;
}
const classNames = createStaticStyles(({ css }) => ({
const useStyle = createStyles(() => ({
iconWrap: css`
display: inline-flex;
align-items: center;
@@ -19,8 +19,9 @@ const classNames = createStaticStyles(({ css }) => ({
const PnpmIcon: React.FC<IconProps> = (props) => {
const { className, style } = props;
const { styles } = useStyle();
return (
<span className={clsx(classNames.iconWrap, className)} style={style}>
<span className={clsx(styles.iconWrap, className)} style={style}>
<svg
aria-hidden="true"
fill="#F69220"

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles, css } from 'antd-style';
import { clsx } from 'clsx';
interface IconProps {
@@ -7,7 +7,7 @@ interface IconProps {
style?: React.CSSProperties;
}
const classNames = createStaticStyles(({ css }) => ({
const useStyle = createStyles(() => ({
iconWrap: css`
display: inline-flex;
align-items: center;
@@ -19,8 +19,9 @@ const classNames = createStaticStyles(({ css }) => ({
const YarnIcon: React.FC<IconProps> = (props) => {
const { className, style } = props;
const { styles } = useStyle();
return (
<span className={clsx(classNames.iconWrap, className)} style={style}>
<span className={clsx(styles.iconWrap, className)} style={style}>
<svg
aria-hidden="true"
fill="#2C8EBB"

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import { UpOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Badge, Tag, Tooltip } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles, css } from 'antd-style';
import { clsx } from 'clsx';
import { FormattedMessage, useLiveDemo, useSiteData } from 'dumi';
import { major, minVersion } from 'semver';
@@ -17,7 +17,7 @@ import DemoContext from '../../slots/DemoContext';
import { isOfficialHost } from '../../utils';
import Actions from './Actions';
const styles = createStaticStyles(({ cssVar, css }) => {
const useStyle = createStyles(({ cssVar }) => {
return {
codeHideBtn: css`
position: sticky;
@@ -68,6 +68,7 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => {
const { pkg } = useSiteData();
const location = useLocation();
const { styles } = useStyle();
const entryName = 'index.tsx';
const entryCode = asset.dependencies[entryName].value;

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Skeleton } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ css, cssVar }) => ({
skeletonWrapper: css`
width: 100% !important;
height: 250px;
@@ -12,6 +12,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
}));
const DemoFallback = () => {
const { styles } = useStyle();
return (
<Skeleton.Node
active

View File

@@ -2,7 +2,7 @@ import type { FC } from 'react';
import React, { useEffect, useRef } from 'react';
import { CheckOutlined, SketchOutlined } from '@ant-design/icons';
import { App } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import copy from '../../../../components/_util/copy';
import { nodeToGroup } from 'html2sketch';
@@ -22,12 +22,12 @@ const locales = {
},
};
const styles = createStaticStyles(({ cssVar, css }) => ({
const useStyle = createStyles(({ cssVar, css }) => ({
wrapper: css`
position: relative;
border: 1px solid ${cssVar.colorBorderSecondary};
border-radius: ${cssVar.borderRadius};
padding: ${cssVar.paddingMD} ${cssVar.paddingLG} calc(${cssVar.paddingMD} * 2);
padding: ${cssVar.paddingMD} ${cssVar.paddingLG} ${cssVar.paddingMD * 2};
margin-bottom: ${cssVar.marginLG};
`,
title: css`
@@ -67,11 +67,12 @@ const styles = createStaticStyles(({ cssVar, css }) => ({
`,
tip: css`
color: ${cssVar.colorTextTertiary};
margin-top: calc(${cssVar.marginMD} * 2);
margin-top: ${cssVar.marginMD * 2};
`,
}));
const DesignPreviewer: FC<AntdPreviewerProps> = ({ children, title, description, tip, asset }) => {
const { styles } = useStyle();
const demoRef = useRef<HTMLDivElement>(null);
const [copied, setCopied] = React.useState<boolean>(false);
const { message } = App.useApp();

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { BugOutlined } from '@ant-design/icons';
import { Button, Flex, Popover, theme } from 'antd';
import { createStaticStyles, cx } from 'antd-style';
import { createStyles } from 'antd-style';
import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs';
@@ -33,7 +33,7 @@ const locales = {
},
};
const styles = createStaticStyles(({ cssVar, css }) => ({
const useStyle = createStyles(({ cssVar, css }) => ({
container: css`
margin-block: ${cssVar.margin};
padding: ${cssVar.padding};
@@ -49,6 +49,8 @@ const styles = createStaticStyles(({ cssVar, css }) => ({
const RefinedChangelog: React.FC<React.PropsWithChildren<RefinedChangelogProps>> = (props) => {
const { version, date, children } = props;
const { styles, cx } = useStyle();
const memoizedValue = React.useMemo<ContextProps>(() => {
const realVersion = version || '0.0.0';
const bugVersionInfo = matchDeprecated(realVersion);

View File

@@ -1,7 +1,7 @@
import React, { Suspense } from 'react';
import type { SandpackSetup } from '@codesandbox/sandpack-react';
import { Skeleton } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { useSearchParams } from 'dumi';
import { version } from '../../../../package.json';
@@ -17,7 +17,7 @@ const root = createRoot(document.getElementById("root"));
root.render(<App />);
`;
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ css, cssVar }) => ({
fallback: css`
width: 100%;
> * {
@@ -32,6 +32,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
}));
const SandpackFallback: React.FC = () => {
const { styles } = useStyle();
return (
<div className={styles.fallback}>
<Skeleton.Node active style={{ height: 500, width: '100%' }}>

View File

@@ -2,13 +2,13 @@
import React from 'react';
import { FastColor } from '@ant-design/fast-color';
import { Flex, theme } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import tokenMeta from 'antd/es/version/token-meta.json';
import { clsx } from 'clsx';
import useLocale from '../../../hooks/useLocale';
const styles = createStaticStyles(({ cssVar, css }) => {
const useStyle = createStyles(({ cssVar, css }) => {
const height = cssVar.controlHeightLG;
const dotSize = height / 5;
@@ -63,6 +63,7 @@ interface ColorCircleProps {
}
const ColorCircle: React.FC<ColorCircleProps> = ({ color }) => {
const { styles } = useStyle();
return (
<Flex align="center" gap={4}>
<div className={styles.dot} style={{ background: color }} />
@@ -78,6 +79,7 @@ export interface TokenCompareProps {
const TokenCompare: React.FC<TokenCompareProps> = (props) => {
const { tokenNames = '' } = props;
const [, lang] = useLocale();
const { styles } = useStyle();
const tokenList = React.useMemo(() => {
const list = tokenNames.split('|');

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { PauseCircleFilled, PlayCircleFilled } from '@ant-design/icons';
import { createStaticStyles } from 'antd-style';
import { createStyles, css } from 'antd-style';
import { clsx } from 'clsx';
const styles = createStaticStyles(({ css, cx, cssVar }) => {
const useStyles = createStyles(({ cx, cssVar }) => {
const play = css`
position: absolute;
inset-inline-end: ${cssVar.paddingLG};
@@ -45,6 +45,7 @@ const VideoPlayer: React.FC<React.HtmlHTMLAttributes<HTMLVideoElement>> = ({
className,
...restProps
}) => {
const { styles } = useStyles();
const videoRef = React.useRef<HTMLVideoElement>(null);
const [playing, setPlaying] = React.useState(false);

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles, css } from 'antd-style';
import { useRouteMeta } from 'dumi';
import useLocale from '../../../hooks/useLocale';
@@ -17,7 +17,7 @@ export interface BehaviorMapProps {
data: BehaviorMapItem;
}
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ cssVar }) => ({
container: css`
width: 100%;
min-height: 600px;
@@ -100,6 +100,7 @@ const locales = {
const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
const chartRef = useRef<HTMLDivElement>(null);
const { styles } = useStyle();
const [locale] = useLocale(locales);
const meta = useRouteMeta();

View File

@@ -1,14 +1,14 @@
import type { FC } from 'react';
import React, { Suspense } from 'react';
import { Skeleton } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import useLocale from '../../../hooks/useLocale';
import type { BehaviorMapProps } from './BehaviorMap';
const InternalBehaviorMap = React.lazy(() => import('./BehaviorMap'));
const styles = createStaticStyles(({ cssVar, css }) => ({
const useStyle = createStyles(({ cssVar, css }) => ({
fallback: css`
width: 100%;
> * {
@@ -32,6 +32,7 @@ const locales = {
};
const BehaviorMapFallback: React.FC = () => {
const { styles } = useStyle();
const [locale] = useLocale(locales);
return (
<div className={styles.fallback}>

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
const styles = createStaticStyles(({ cssVar, css }) => ({
const useStyle = createStyles(({ cssVar, css }) => ({
browserMockup: css`
position: relative;
border-top: 2em solid rgba(230, 230, 230, 0.7);
@@ -49,6 +49,7 @@ const styles = createStaticStyles(({ cssVar, css }) => ({
}));
const BrowserFrame: React.FC<React.PropsWithChildren> = ({ children }) => {
const { styles } = useStyle();
return <div className={styles.browserMockup}>{children}</div>;
};

View File

@@ -72,7 +72,6 @@ const useStyle = createStyles(({ cssVar, token, css }) => ({
font-weight: 600;
font-size: 20px;
margin: 0 !important;
padding: 0;
`,
versionTag: css`
user-select: none;

View File

@@ -1,11 +1,11 @@
import type { ComponentProps, FC } from 'react';
import React from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import SourceCodeEditor from 'dumi/theme-default/slots/SourceCodeEditor';
import LiveError from '../slots/LiveError';
const styles = createStaticStyles(({ cssVar, css }) => {
const useStyle = createStyles(({ cssVar, css }) => {
return {
editor: css`
// override dumi editor styles
@@ -54,6 +54,7 @@ const LiveCode: FC<
error: Error | null;
} & Pick<ComponentProps<typeof SourceCodeEditor>, 'lang' | 'initialValue' | 'onChange'>
> = (props) => {
const { styles } = useStyle();
return (
<div className={styles.editor}>
<SourceCodeEditor

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { StyleProvider } from '@ant-design/cssinjs';
import { ConfigProvider, Flex, Skeleton, Spin } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { useLocation } from 'dumi';
import { Common } from './styles';
const styles = createStaticStyles(({ css, cssVar }) => {
const useStyle = createStyles(({ css, cssVar }) => {
return {
skeletonWrapper: css`
width: 100%;
@@ -27,6 +27,8 @@ const styles = createStaticStyles(({ css, cssVar }) => {
const Loading: React.FC = () => {
const { pathname } = useLocation();
const { styles } = useStyle();
let loadingNode: React.ReactNode = null;
if (

View File

@@ -5,35 +5,36 @@ import { clsx } from 'clsx';
const useStyle = createStyles(({ cssVar, cx }) => {
const duration = cssVar.motionDurationSlow;
const marker = css({
'--mark-border-size': '1px',
position: 'absolute',
border: `var(--mark-border-size) solid ${cssVar.colorWarning}`,
boxSizing: 'border-box',
zIndex: 999999,
pointerEvents: 'none',
left: 'calc(var(--rect-left) * 1px - var(--mark-border-size))',
top: 'calc(var(--rect-top) * 1px - var(--mark-border-size))',
width: 'calc(var(--rect-width) * 1px + var(--mark-border-size) * 2)',
height: 'calc(var(--rect-height) * 1px + var(--mark-border-size) * 2)',
opacity: 0,
transition: `all ${duration} ease`,
});
const marker = css`
--mark-border-size: 1px;
position: absolute;
border: var(--mark-border-size) solid ${cssVar.colorWarning};
box-sizing: border-box;
z-index: 999999;
pointer-events: none;
left: calc(var(--rect-left) * 1px - var(--mark-border-size));
top: calc(var(--rect-top) * 1px - var(--mark-border-size));
width: calc(var(--rect-width) * 1px + var(--mark-border-size) * 2);
height: calc(var(--rect-height) * 1px + var(--mark-border-size) * 2);
const markerActive = css({
[`&.${cx(marker)}`]: {
opacity: 0.875,
},
});
opacity: 0;
transition: all ${duration} ease;
`;
const markerPrimary = css({
[`&.${cx(marker)}.${cx(markerActive)}`]: {
'--mark-border-size': '2px',
opacity: 1,
boxShadow: '0 0 0 1px #fff',
zIndex: 1000000,
},
});
const markerActive = css`
&.${cx(marker)} {
opacity: 0.85;
}
`;
const markerPrimary = css`
&.${cx(marker)}.${cx(markerActive)} {
--mark-border-size: 2px;
opacity: 1;
box-shadow: 0 0 0 1px #fff;
z-index: 1000000;
}
`;
return {
marker,

View File

@@ -67,14 +67,11 @@ const Block: React.FC<BlockProps> = ({
...props
}) => {
const divRef = React.useRef<HTMLDivElement>(null);
// 多选模式下,优先使用 multipleProps 中的 defaultValue
const multipleDefaultValue = (multipleProps as any)?.defaultValue;
const initialValue = mode === 'single' ? defaultValue : multipleDefaultValue;
const [value, setValue] = React.useState(initialValue);
const [value, setValue] = React.useState(defaultValue);
React.useEffect(() => {
setValue(mode === 'single' ? defaultValue : multipleDefaultValue);
}, [mode, defaultValue, multipleDefaultValue]);
setValue(defaultValue);
}, [mode]);
return (
<Flex
@@ -104,7 +101,7 @@ const Block: React.FC<BlockProps> = ({
options={options}
{...(mode === 'multiple' ? multipleProps : {})}
styles={{ popup: { zIndex: 1 } }}
maxTagCount={process.env.NODE_ENV === 'test' ? 1 : 'responsive'}
maxTagCount="responsive"
placeholder="Please select"
allowClear
/>

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { InfoCircleOutlined, PushpinOutlined } from '@ant-design/icons';
import { get, set } from '@rc-component/util';
import { Button, Col, ConfigProvider, Flex, Popover, Row, Tag, theme, Typography } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles, css } from 'antd-style';
import { clsx } from 'clsx';
import Prism from 'prismjs';
@@ -12,7 +12,7 @@ export interface SemanticPreviewInjectionProps {
classNames?: Record<string, string>;
}
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ cssVar }) => ({
container: css`
position: relative;
z-index: 0;
@@ -122,7 +122,6 @@ export interface SemanticPreviewProps {
height?: number;
padding?: false;
style?: React.CSSProperties;
motion?: boolean;
}
const SemanticPreview: React.FC<SemanticPreviewProps> = (props) => {
@@ -134,7 +133,6 @@ const SemanticPreview: React.FC<SemanticPreviewProps> = (props) => {
style,
componentName = 'Component',
itemsAPI,
motion = false,
} = props;
const { token } = theme.useToken();
@@ -157,6 +155,8 @@ const SemanticPreview: React.FC<SemanticPreviewProps> = (props) => {
const mergedSemantic = pinSemantic || hoverSemantic;
const { styles } = useStyle();
const hoveredSemanticClassNames = React.useMemo(() => {
if (!mergedSemantic) {
return semanticClassNames;
@@ -185,7 +185,7 @@ const SemanticPreview: React.FC<SemanticPreviewProps> = (props) => {
className={clsx(styles.colWrap, padding === false && styles.colWrapPaddingLess)}
style={style}
>
<ConfigProvider theme={{ token: { motion } }}>{cloneNode}</ConfigProvider>
<ConfigProvider theme={{ token: { motion: false } }}>{cloneNode}</ConfigProvider>
</Col>
<Col span={8}>
<ul className={clsx(styles.listWrap)}>

View File

@@ -53,21 +53,17 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
const nextItems: GetProp<typeof Bubble.List, 'items'> = [
{
key: 1,
role: 'user',
placement: 'end',
content: prompt,
avatar: <UserOutlined />,
avatar: { icon: <UserOutlined /> },
shape: 'corner',
},
{
key: 2,
role: 'system',
placement: 'start',
content: resText,
avatar: <AntDesignOutlined />,
avatar: { icon: <AntDesignOutlined /> },
loading: !resText,
contentRender: (content: string) => (
messageRender: (content: string) => (
<Typography>
<pre style={{ margin: 0 }}>{content}</pre>
</Typography>
@@ -77,11 +73,9 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
if (!loading) {
nextItems.push({
key: 3,
role: 'divider',
placement: 'start',
content: locale.finishTips,
avatar: <AntDesignOutlined />,
avatar: { icon: <AntDesignOutlined /> },
shape: 'corner',
});
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable compat/compat */
import { useRef, useState } from 'react';
import { XStream } from '@ant-design/x-sdk';
import { XStream } from '@ant-design/x';
import type { SiteContextProps } from '../../../theme/slots/SiteContext';

View File

@@ -61,14 +61,11 @@ const Block: React.FC<BlockProps> = ({
...props
}) => {
const divRef = React.useRef<HTMLDivElement>(null);
// 多选模式下,优先使用 multipleProps 中的 defaultValue
const multipleDefaultValue = (multipleProps as any)?.defaultValue;
const initialValue = mode === 'single' ? defaultValue : multipleDefaultValue;
const [value, setValue] = React.useState(initialValue);
const [value, setValue] = React.useState(defaultValue);
React.useEffect(() => {
setValue(mode === 'single' ? defaultValue : multipleDefaultValue);
}, [mode, defaultValue, multipleDefaultValue]);
setValue(defaultValue);
}, [mode]);
return (
<Flex
@@ -96,7 +93,7 @@ const Block: React.FC<BlockProps> = ({
treeData={treeData}
{...(mode === 'multiple' ? multipleProps : {})}
styles={{ popup: { zIndex: 1 } }}
maxTagCount={process.env.NODE_ENV === 'test' ? 1 : 'responsive'}
maxTagCount="responsive"
placeholder="Please select"
/>
</Flex>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import useLocale from '../../../hooks/useLocale';
import EN from './en-US.md';
@@ -7,7 +7,7 @@ import CN from './zh-CN.md';
const changeLog = { cn: CN, en: EN };
const classNames = createStaticStyles(({ css }) => ({
const useStyle = createStyles(({ css }) => ({
container: css`
max-height: max(62vh, 500px);
overflow-y: scroll;
@@ -25,11 +25,13 @@ const classNames = createStaticStyles(({ css }) => ({
const ChangeLog = () => {
const [, lang] = useLocale();
const { styles } = useStyle();
const validatedLanguage = Object.keys(changeLog).includes(lang) ? lang : 'en';
const C = changeLog[validatedLanguage];
return (
<div className={classNames.container}>
<div className={styles.container}>
<C />
</div>
);

View File

@@ -29,13 +29,13 @@ const VersionUpgradeModal = () => {
const [locale, lang] = useLocale(locales);
const { pathname } = useLocation();
const [open, setOpen] = React.useState(false);
const [open, updateOpen] = React.useState(false);
const isCN = lang === 'cn' || utils.isZhCN(pathname);
function handleClose() {
localStorage.setItem(STORAGE_KEY, Date.now().toString());
setOpen(false);
updateOpen(false);
}
React.useEffect(() => {
@@ -48,7 +48,7 @@ const VersionUpgradeModal = () => {
if (!lastTime) {
const timer = setTimeout(() => {
setOpen(true);
updateOpen(true);
}, 1000);
return () => {

View File

@@ -1,13 +1,13 @@
import type { PropsWithChildren } from 'react';
import React from 'react';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { useSearchParams } from 'dumi';
import CommonHelmet from '../../common/CommonHelmet';
import Content from '../../slots/Content';
import Sidebar from '../../slots/Sidebar';
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ css, cssVar }) => ({
main: css`
display: flex;
margin-top: ${cssVar.marginXL};
@@ -15,6 +15,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
}));
const SidebarLayout: React.FC<PropsWithChildren> = ({ children }) => {
const { styles } = useStyle();
const [searchParams] = useSearchParams();
const hideLayout = searchParams.get('layout') === 'false';
return (

View File

@@ -1,17 +1,242 @@
import type { IApi } from 'dumi';
import { createHash } from 'crypto';
import fs from 'fs';
import path from 'path';
import createEmotionServer from '@emotion/server/create-instance';
import type { IApi, IRoute } from 'dumi';
import ReactTechStack from 'dumi/dist/techStacks/react';
import tsToJs from './utils/tsToJs';
import buildAssetsPlugin from './plugins/build-assets';
import llmsPlugin from './plugins/llms';
import rawMdPlugin from './plugins/raw-md';
import routesPlugin from './plugins/routes';
import semanticMdPlugin from './plugins/semantic-md';
import techStackPlugin from './plugins/tech-stack';
import { dependencies, devDependencies } from '../../package.json';
export default async function plugin(api: IApi) {
techStackPlugin(api);
routesPlugin(api);
await buildAssetsPlugin(api);
rawMdPlugin(api);
semanticMdPlugin(api);
llmsPlugin(api);
function extractEmotionStyle(html: string) {
// copy from emotion ssr
// https://github.com/vercel/next.js/blob/deprecated-main/examples/with-emotion-vanilla/pages/_document.js
const styles = global.__ANTD_STYLE_CACHE_MANAGER_FOR_SSR__.getCacheList().map((cache) => {
const result = createEmotionServer(cache).extractCritical(html);
if (!result.css) {
return null;
}
const { css, ids } = result;
return {
key: cache.key,
css,
ids,
tag: `<style data-emotion="${cache.key} ${result.ids.join(' ')}">${result.css}</style>`,
};
});
return styles.filter(Boolean);
}
export const getHash = (str: string, length = 8) =>
createHash('md5').update(str).digest('hex').slice(0, length);
/**
* extends dumi internal tech stack, for customize previewer props
*/
class AntdReactTechStack extends ReactTechStack {
generatePreviewerProps(...[props, opts]: any) {
props.pkgDependencyList = { ...devDependencies, ...dependencies };
props.jsx ??= '';
if (opts.type === 'code-block') {
props.jsx = opts?.entryPointCode ? tsToJs(opts.entryPointCode) : '';
}
if (opts.type === 'external') {
// try to find md file with the same name as the demo tsx file
const locale = opts.mdAbsPath.match(/index\.([a-z-]+)\.md$/i)?.[1];
const mdPath = opts.fileAbsPath!.replace(/\.\w+$/, '.md');
const md = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : '';
const codePath = opts.fileAbsPath!.replace(/\.\w+$/, '.tsx');
const code = fs.existsSync(codePath) ? fs.readFileSync(codePath, 'utf-8') : '';
props.jsx = tsToJs(code);
if (md) {
// extract description & css style from md file
const blocks: Record<string, string> = {};
const lines = md.split('\n');
let blockName = '';
let cacheList: string[] = [];
// Get block name
const getBlockName = (text: string) => {
if (text.startsWith('## ')) {
return text.replace('## ', '').trim();
}
if (text.startsWith('```css') || text.startsWith('<style>')) {
return 'style';
}
return null;
};
// Fill block content
const fillBlock = (name: string, lineList: string[]) => {
if (lineList.length) {
let fullText: string;
if (name === 'style') {
fullText = lineList
.join('\n')
.replace(/<\/?style>/g, '')
.replace(/```(\s*css)/g, '');
} else {
fullText = lineList.slice(1).join('\n');
}
blocks[name] = fullText;
}
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Mark as new block
const nextBlockName = getBlockName(line);
if (nextBlockName) {
fillBlock(blockName, cacheList);
// Next Block
blockName = nextBlockName;
cacheList = [line];
} else {
cacheList.push(line);
}
}
// Last block
fillBlock(blockName, cacheList);
props.description = blocks[locale];
props.style = blocks.style;
}
}
return props;
}
}
const resolve = (p: string): string => require.resolve(p);
const RoutesPlugin = async (api: IApi) => {
const chalk = await import('chalk').then((m) => m.default);
// const ssrCssFileName = `ssr-${Date.now()}.css`;
const writeCSSFile = (key: string, hashKey: string, cssString: string) => {
const fileName = `style-${key}.${getHash(hashKey)}.css`;
const filePath = path.join(api.paths.absOutputPath, fileName);
if (!fs.existsSync(filePath)) {
api.logger.event(chalk.grey(`write to: ${filePath}`));
fs.writeFileSync(filePath, cssString, 'utf8');
}
return fileName;
};
const addLinkStyle = (html: string, cssFile: string, prepend = false) => {
const prefix = api.userConfig.publicPath || api.config.publicPath;
if (prepend) {
return html.replace('<head>', `<head><link rel="stylesheet" href="${prefix + cssFile}">`);
}
return html.replace('</head>', `<link rel="stylesheet" href="${prefix + cssFile}"></head>`);
};
api.registerTechStack(() => new AntdReactTechStack());
api.modifyRoutes((routes) => {
// TODO: append extra routes, such as home, changelog, form-v3
/**
* **important!** Make sure that the `id` and `path` are consistent.
* see: https://github.com/ant-design/ant-design/issues/55960
*/
const extraRoutesList: IRoute[] = [
{
id: 'changelog',
path: 'changelog',
absPath: '/changelog',
parentId: 'DocLayout',
file: resolve('../../CHANGELOG.en-US.md'),
},
{
id: 'changelog-cn',
path: 'changelog-cn',
absPath: '/changelog-cn',
parentId: 'DocLayout',
file: resolve('../../CHANGELOG.zh-CN.md'),
},
{
id: 'components/changelog',
path: 'components/changelog',
absPath: '/components/changelog',
parentId: 'DocLayout',
file: resolve('../../CHANGELOG.en-US.md'),
},
{
id: 'components/changelog-cn',
path: 'components/changelog-cn',
absPath: '/components/changelog-cn',
parentId: 'DocLayout',
file: resolve('../../CHANGELOG.zh-CN.md'),
},
];
extraRoutesList.forEach((itemRoute) => {
routes[itemRoute.path] = itemRoute;
});
return routes;
});
api.modifyExportHTMLFiles((files) =>
files
// exclude dynamic route path, to avoid deploy failed by `:id` directory
.filter((f) => !f.path.includes(':'))
.map((file) => {
// 1. 提取 antd-style 样式
const styles = extractEmotionStyle(file.content);
// 2. 提取每个样式到独立 css 文件
styles.forEach((result) => {
api.logger.event(
`${chalk.yellow(file.path)} include ${chalk.blue`[${result!.key}]`} ${chalk.yellow(
result!.ids.length,
)} styles`,
);
const cssFile = writeCSSFile(result!.key, result!.ids.join(''), result!.css);
file.content = addLinkStyle(file.content, cssFile);
});
return file;
}),
);
// add ssr css file to html
api.modifyConfig((memo) => {
memo.styles ??= [];
// memo.styles.push(`/${ssrCssFileName}`);
return memo;
});
if (process.env.NODE_ENV === 'production') {
api.addEntryImportsAhead(() => ({
source: path.join(api.paths.cwd, 'components', 'style', 'antd.css'),
}));
}
};
export default RoutesPlugin;

View File

@@ -1,98 +0,0 @@
import { createHash } from 'crypto';
import fs from 'fs';
import path from 'path';
import createEmotionServer from '@emotion/server/create-instance';
import type { IApi } from 'dumi';
export const getHash = (str: string, length = 8) =>
createHash('md5').update(str).digest('hex').slice(0, length);
function extractEmotionStyle(html: string) {
// copy from emotion ssr
// https://github.com/vercel/next.js/blob/deprecated-main/examples/with-emotion-vanilla/pages/_document.js
const styles = global.__ANTD_STYLE_CACHE_MANAGER_FOR_SSR__.getCacheList().map((cache) => {
const result = createEmotionServer(cache).extractCritical(html);
if (!result.css) {
return null;
}
const { css, ids } = result;
return {
key: cache.key,
css,
ids,
tag: `<style data-emotion="${cache.key} ${result.ids.join(' ')}">${result.css}</style>`,
};
});
return styles.filter(Boolean);
}
export default async function buildAssetsPlugin(api: IApi) {
const chalk = await import('chalk').then((m) => m.default);
const writeCSSFile = (key: string, hashKey: string, cssString: string) => {
const fileName = `style-${key}.${getHash(hashKey)}.css`;
const filePath = path.join(api.paths.absOutputPath, fileName);
if (!fs.existsSync(filePath)) {
api.logger.event(chalk.grey(`write to: ${filePath}`));
fs.writeFileSync(filePath, cssString, 'utf8');
}
return fileName;
};
const addLinkStyle = (html: string, cssFile: string, prepend = false) => {
const prefix = api.userConfig.publicPath || api.config.publicPath;
if (prepend) {
return html.replace('<head>', `<head><link rel="stylesheet" href="${prefix + cssFile}">`);
}
return html.replace('</head>', `<link rel="stylesheet" href="${prefix + cssFile}"></head>`);
};
api.modifyExportHTMLFiles((files) =>
files
// exclude dynamic route path, to avoid deploy failed by `:id` directory
.filter((f) => !f.path.includes(':'))
.map((file) => {
// 1. 提取 antd-style 样式
const styles = extractEmotionStyle(file.content);
// 2. 提取每个样式到独立 css 文件
styles.forEach((result) => {
api.logger.event(
`${chalk.yellow(file.path)} include ${chalk.blue`[${result!.key}]`} ${chalk.yellow(
result!.ids.length,
)} styles`,
);
const cssFile = writeCSSFile(result!.key, result!.ids.join(''), result!.css);
file.content = addLinkStyle(file.content, cssFile);
});
return file;
}),
);
// add ssr css file to html
api.modifyConfig((memo) => {
memo.styles ??= [];
return memo;
});
if (process.env.NODE_ENV === 'production') {
// `addEntryImportsAhead` do not support compile,
// so it will build file content directly without compile.
// We add additional pre-site script for this,
// but this will not affect normal developer usage.
api.addEntryImportsAhead(() => ({
source: path.join(api.paths.cwd, 'components', 'style', '~antd.layer.css'),
}));
}
}

View File

@@ -1,284 +0,0 @@
import fs from 'fs';
import path from 'path';
import type { IApi } from 'dumi';
import { glob } from 'glob';
interface DocItem {
title: string;
url: string;
category: 'docs' | 'component' | 'semantic';
content?: string;
}
interface ProcessResult {
docs: DocItem[];
components: DocItem[];
semantics: DocItem[];
}
function processMarkdownFile(
markdownFile: string,
siteDir: string,
targetArrays: ProcessResult,
): void {
const mdPath = path.join(siteDir, markdownFile);
const content = fs.readFileSync(mdPath, 'utf-8').trim();
if (!content) {
return;
}
// Extract title from first H1 heading
const titleMatch = content.match(/^#\s+(.+)$/m);
const title = titleMatch ? titleMatch[1].trim() : path.basename(markdownFile, '.md');
// Generate URL from file path (use .md suffix)
let urlPath = markdownFile.replace(/\.md$/, '');
// Remove /index suffix for component pages
if (urlPath.endsWith('/index')) {
urlPath = urlPath.replace(/\/index$/, '');
}
const url = `https://ant.design/${urlPath}.md`;
// Categorize files
if (/\/semantic.*\.md$/.test(markdownFile)) {
// Component semantic files
const componentName = path.basename(path.dirname(markdownFile));
const semanticFileName = path.basename(markdownFile, '.md');
// 提取 semantic 后缀(如 semantic_ribbon -> Ribbon
const semanticSuffix = semanticFileName.replace(/^semantic/, '').replace(/^_/, '');
const displaySuffix = semanticSuffix
? semanticSuffix
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('')
: '';
targetArrays.semantics.push({
title: `${componentName}${displaySuffix ? ` ${displaySuffix}` : ''} Semantic`,
url,
category: 'semantic',
content,
});
} else if (markdownFile.startsWith('components/')) {
// Component documentation files
const componentName = path.basename(markdownFile, '.md');
targetArrays.components.push({
title: componentName,
url,
category: 'component',
content,
});
} else if (markdownFile.startsWith('docs/') || markdownFile.startsWith('changelog')) {
// Documentation files
targetArrays.docs.push({
title,
url,
category: 'docs',
content,
});
}
}
async function generateLLms(api: IApi) {
const siteDir = api.paths.absOutputPath;
// Ensure siteDir exists
if (!fs.existsSync(siteDir)) {
api.logger.error('Error: Output directory does not exist. Please run build first.');
return;
}
// Find all markdown files in _site, excluding llms files
const markdownFiles = await glob('**/*.md', {
cwd: siteDir,
ignore: ['llms*.md', 'llms*.txt'],
});
// Separate English and Chinese docs
const englishDocs = markdownFiles.filter(
(file) => !file.includes('-cn/') && !file.endsWith('-cn.md'),
);
const chineseDocs = markdownFiles.filter(
(file) => file.includes('-cn/') || file.endsWith('-cn.md'),
);
const englishResult: ProcessResult = {
docs: [],
components: [],
semantics: [],
};
const chineseResult: ProcessResult = {
docs: [],
components: [],
semantics: [],
};
// Process English docs
for (const markdownFile of englishDocs) {
try {
processMarkdownFile(markdownFile, siteDir, englishResult);
} catch (error) {
api.logger.warn(`Error processing ${markdownFile}:`, error);
}
}
// Process Chinese docs
for (const markdownFile of chineseDocs) {
try {
processMarkdownFile(markdownFile, siteDir, chineseResult);
} catch (error) {
api.logger.warn(`Error processing ${markdownFile}:`, error);
}
}
const { docs, components, semantics } = englishResult;
const { docs: docsCn, components: componentsCn, semantics: semanticsCn } = chineseResult;
// Sort by title
docs.sort((a, b) => a.title.localeCompare(b.title));
components.sort((a, b) => a.title.localeCompare(b.title));
semantics.sort((a, b) => a.title.localeCompare(b.title));
docsCn.sort((a, b) => a.title.localeCompare(b.title));
componentsCn.sort((a, b) => a.title.localeCompare(b.title));
semanticsCn.sort((a, b) => a.title.localeCompare(b.title));
// 1. Generate llms-semantic.md
const semanticContent = [
'# Ant Design Semantic Documentation',
'',
'This file contains aggregated semantic descriptions for all components.',
'',
`> Total ${semantics.length} components contain semantic descriptions`,
'',
...semantics.flatMap((semantic) => [
`# ${semantic.title}`,
'',
`Source: ${semantic.url}`,
'',
semantic.content || '',
'',
'---',
'',
]),
].join('\n');
// 2. Generate llms-semantic-cn.md
const semanticContentCn = [
'# Ant Design 组件语义化描述',
'',
'本文档包含了 Ant Design 组件库中所有组件的语义化描述信息。',
'',
`> 总计 ${semanticsCn.length} 个组件包含语义化描述`,
'',
...semanticsCn.flatMap((semantic) => [
`# ${semantic.title}`,
'',
`Source: ${semantic.url}`,
'',
semantic.content || '',
'',
'---',
'',
]),
].join('\n');
// 3. Generate llms-full.txt
const fullContent = [
'# Ant Design Component Documentation',
'',
'This file contains aggregated content from all component docs.',
'',
`> Total ${components.length} components`,
'',
...components.flatMap((component) => [
`## ${component.title}`,
'',
`Source: ${component.url}`,
'',
component.content || '',
'',
'---',
'',
]),
].join('\n');
// 4. Generate llms-full-cn.txt
const fullContentCn = [
'# Ant Design 组件文档',
'',
'本文件包含所有组件文档的聚合内容。',
'',
`> 总计 ${componentsCn.length} 个组件`,
'',
...componentsCn.flatMap((component) => [
`## ${component.title}`,
'',
`Source: ${component.url}`,
'',
component.content || '',
'',
'---',
'',
]),
].join('\n');
// 5. Generate llms.txt
const llmsNavContent = [
'# Ant Design - Enterprise-class React UI library',
'',
'- Ant Design, developed by Ant Group, is a React UI library that aims to provide a high-quality design language and development framework for enterprise-level backend management systems. It offers a rich set of components and design guidelines, helping developers build modern, responsive, and high-performance web applications.',
'',
'## Navigation',
'',
'- [Full Documentation (EN)](./llms-full.txt)',
'- [Full Documentation (CN)](./llms-full-cn.txt)',
'- [Semantic Documentation (EN)](./llms-semantic.md)',
'- [Semantic Documentation (CN)](./llms-semantic-cn.md)',
'',
'## Docs (EN)',
'',
...docs.map(({ title, url }) => `- [${title}](${url})`),
'',
'## Docs (CN)',
'',
...docsCn.map(({ title, url }) => `- [${title}](${url})`),
'',
'## Component Docs (EN)',
'',
...components.map(({ title, url }) => `- [${title}](${url})`),
'',
'## Component Docs (CN)',
'',
...componentsCn.map(({ title, url }) => `- [${title}](${url})`),
'',
'## Semantic (EN)',
'',
...semantics.map(({ title, url }) => `- [${title}](${url})`),
'',
'## Semantic (CN)',
'',
...semanticsCn.map(({ title, url }) => `- [${title}](${url})`),
'',
].join('\n');
// Write all files
fs.writeFileSync(path.join(siteDir, 'llms-semantic.md'), semanticContent);
fs.writeFileSync(path.join(siteDir, 'llms-semantic-cn.md'), semanticContentCn);
fs.writeFileSync(path.join(siteDir, 'llms-full.txt'), fullContent);
fs.writeFileSync(path.join(siteDir, 'llms-full-cn.txt'), fullContentCn);
fs.writeFileSync(path.join(siteDir, 'llms.txt'), llmsNavContent);
api.logger.event(
`Generated llms.txt (navigation), llms-full.txt (${components.length} components), llms-full-cn.txt (${componentsCn.length} components), llms-semantic.md (${semantics.length} semantics), llms-semantic-cn.md (${semanticsCn.length} semantics)`,
);
}
export default async function llmsPlugin(api: IApi) {
api.modifyExportHTMLFiles(async (files) => {
await generateLLms(api);
return files;
});
}

View File

@@ -1,626 +0,0 @@
import fs from 'fs';
import path from 'path';
import type { IApi } from 'dumi';
type TokenMeta = {
components?: Record<
string,
Array<{ token: string; desc?: string; descEn?: string; type?: string }>
>;
global?: Record<
string,
{
desc?: string;
descEn?: string;
type?: string;
name?: string;
nameEn?: string;
}
>;
};
type TokenData = Record<string, { component?: Record<string, unknown>; global?: string[] }>;
/**
* 路由信息
*/
interface RouteInfo {
absPath: string;
file: string;
}
/**
* 内容过滤器上下文信息
*/
interface ContentFilterContext {
route: RouteInfo;
file: string;
absPath: string;
relPath: string;
api: IApi;
}
/**
* 插件配置选项
*/
export interface PluginOptions {
/**
* 路由过滤器:决定哪些路由需要生成 markdown
* @param route - 路由信息
* @returns 返回 true 表示处理该路由false 表示跳过
*/
routeFilter?: (route: RouteInfo) => boolean;
/**
* 内容替换器数组:在处理内容前可以替换或修改内容,按顺序链式应用
* @param content - 原始 markdown 内容
* @param context - 替换器上下文信息包含路由、文件路径、API 等
* @returns 返回处理后的内容,如果返回 null 或空字符串则跳过该路由
*/
contentReplacers?: Array<(content: string, context: ContentFilterContext) => string | null>;
/**
* 是否启用替换 <code src> 标签功能,默认为 true
*/
enableReplaceCodeSrc?: boolean;
/**
* 代码追加函数:在替换 <code src> 标签时,用于追加额外的内容(如 demo 描述信息)
* @param docFileAbs - 文档文件的绝对路径
* @returns 返回要追加的 markdown 内容字符串
*/
codeAppend?: (docFileAbs: string, src: string) => string;
}
/**
* 收集的 raw markdown 路由列表
*/
let RAW_MD_ROUTES: Array<RouteInfo> = [];
/**
* 插件配置
*/
let PLUGIN_OPTIONS: PluginOptions = {};
/**
* 记录 raw markdown 文档是否已经输出过
*/
let RAW_MD_EMITTED = false;
/**
* Token 数据缓存
* 避免重复读取文件,提升性能
*/
let TOKEN_CACHE: { meta: TokenMeta; data: TokenData } | null | undefined;
/**
* 读取 JSON 文件,如果文件不存在或解析失败则返回 null
*
* @param abs - JSON 文件的绝对路径
* @returns 解析后的 JSON 对象,如果文件不存在或解析失败则返回 null
*/
function readJsonIfExists<T>(abs: string): T | null {
try {
if (!fs.existsSync(abs)) {
return null;
}
return JSON.parse(fs.readFileSync(abs, 'utf-8')) as T;
} catch {
return null;
}
}
/**
* 根据文档文件路径检测文档语言
* 通过文件名后缀判断是中文还是英文文档
*
* @param docFileAbs - 文档文件的绝对路径
* @returns 返回检测到的语言,默认为 'en-US'
*/
function detectDocLocale(docFileAbs: string): 'zh-CN' | 'en-US' {
if (/-cn\.md$/i.test(docFileAbs) || /\.zh-CN\.md$/i.test(docFileAbs)) {
return 'zh-CN';
}
return 'en-US';
}
/**
* 替换 markdown 中的 prettier-ignore 注释为空
* 用于清理文档内容,移除 `<!-- prettier-ignore -->` 格式的注释
*
* @param md - 原始 markdown 内容
* @returns 替换 prettier-ignore 注释后的 markdown 内容
*/
function replacePrettierIgnore(md: string) {
return md.replace(/<!--\s*prettier-ignore\s*-->\s*\n?/g, '');
}
/**
* 替换 markdown 中的 "Semantic DOM" 部分的 code 标签为指向生成的 semantic.md 文件的链接
*
* @param md - 原始 markdown 内容
* @param context - 内容过滤器上下文信息
* @returns 替换后的 markdown 内容
*/
function replaceSemanticDomSection(md: string, context: ContentFilterContext) {
// 从文档路径推断组件路径(用于生成链接)
// 例如components/card/index.en-US.md -> components/card/semantic.md
const componentPathMatch = context.file.match(/components\/([^/]+)\//);
if (!componentPathMatch) {
return md;
}
const componentName = componentPathMatch[1];
const isZhCN = /-cn\.md$/i.test(context.file) || /\.zh-CN\.md$/i.test(context.file);
const componentPath = `components/${componentName}${isZhCN ? '-cn' : ''}`;
// 匹配 <code src="./demo/_semantic*.tsx"> 标签并替换为 URL 地址
return md.replace(/<code[^>]*_semantic[^>]*>.*?<\/code>/g, (match) => {
// 从匹配的标签中提取文件名
const demoIndex = match.indexOf('./demo/');
if (demoIndex === -1) {
return match;
}
const start = demoIndex + './demo/'.length;
const end = match.indexOf('"', start);
if (end === -1) {
return match;
}
const semanticFile = match.substring(start, end);
// 生成对应的 semantic.md 文件名_semantic.tsx -> semantic.md, _semantic_meta.tsx -> semantic_meta.md
const semanticMdFileName = semanticFile
.replace(/^_semantic/, 'semantic')
.replace(/\.tsx$/, '.md');
return `https://ant.design/${componentPath}/${semanticMdFileName}`;
});
}
/**
* 获取文本中最大连续反引号的数量
* 用于确定代码块围栏所需的反引号数量,避免代码块内部的反引号与围栏冲突
*
* @param text - 待检查的文本内容
* @returns 文本中最大连续反引号的数量
*/
function getMaxBacktickRun(text: string) {
let max = 0;
const re = /`+/g;
let m: RegExpExecArray | null = re.exec(text);
while (m) {
if (m[0].length > max) {
max = m[0].length;
}
m = re.exec(text);
}
return max;
}
/**
* 将代码包装为 markdown 代码块格式
* 自动根据代码内容中的反引号数量确定围栏长度,避免代码块溢出
*
* @param code - 待包装的代码内容
* @param lang - 代码块的语言标识符(如 'tsx', 'js', 'css' 等),默认为空字符串
* @returns 格式化后的 markdown 代码块
*/
function wrapFencedCode(code: string, lang = '') {
const maxTicks = getMaxBacktickRun(code);
const fence = '`'.repeat(Math.max(3, maxTicks + 1));
const head = lang ? `${fence}${lang}` : fence;
return `${head}\n${code.trimEnd()}\n${fence}`;
}
/**
* 根据文件扩展名获取代码块语言标识
* @param filePath - 文件路径
* @returns 代码块语言标识
*/
function getCodeLang(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const langMap: Record<string, string> = {
'.tsx': 'tsx',
'.ts': 'typescript',
'.jsx': 'jsx',
'.js': 'javascript',
'.vue': 'vue',
'.css': 'css',
'.less': 'less',
'.scss': 'scss',
'.sass': 'sass',
'.json': 'json',
'.html': 'html',
'.md': 'markdown',
};
return langMap[ext] || ext.slice(1) || '';
}
/**
* 从 markdown 内容中提取指定语言的块
* 用于处理多语言文档,提取特定语言版本的标题块内容
*
* @param docFileAbs - 文档文件的绝对路径
* @returns 提取的指定语言块内容,如果未找到则返回整个文档的 trim 结果
*/
function antdCodeAppend(docFileAbs: string, src: string): string {
const docDir = path.dirname(docFileAbs);
const locale = detectDocLocale(docFileAbs);
const demoAbs = path.resolve(docDir, src);
const demoMdAbs = demoAbs.replace(path.extname(src), '.md');
const demoMd = fs.existsSync(demoMdAbs) ? fs.readFileSync(demoMdAbs, 'utf-8') : '';
const other = locale === 'zh-CN' ? 'en-US' : 'zh-CN';
const re = new RegExp(
`(^|\\n)##\\s*${locale}\\s*\\n([\\s\\S]*?)(?=\\n##\\s*${other}\\s*\\n|$)`,
'i',
);
const match = demoMd.match(re);
if (!match) {
return demoMd.trim();
}
return (match[2] ?? '').trim();
}
/**
* 替换 <code src> 标签为 markdown 代码块
* 将 `<code src="./demo/basic.tsx" version="5.21.0">标题</code>` 替换为完整的 demo 代码块
* 支持读取对应的 .md 文件作为 demo 描述,并根据文档语言提取对应语言块
*
* @param md - 原始 markdown 内容
* @param docFileAbs - 文档文件的绝对路径,用于解析相对路径和检测语言
* @param codeAppend - 代码追加函数:在替换 <code src> 标签时,用于追加额外的内容(如 demo 描述信息)
* @returns 替换后的 markdown 内容
*/
function replaceCodeSrcToMarkdown(
md: string,
docFileAbs: string,
codeAppend?: (docFileAbs: string, src: string) => string,
) {
const docDir = path.dirname(docFileAbs);
// 匹配 <code src="./demo/basic.tsx">标题</code> 格式的标签
const codeTagRE = /<code\s+[^>]*?src="([^"]+)"[^>]*>([\s\S]*?)<\/code>/g;
return md.replace(codeTagRE, (full, src, title) => {
try {
// 如果标记了 debug 属性,直接去除不显示
if (full.includes('debug')) {
return '';
}
const parts: string[] = [];
const demoAbs = path.resolve(docDir, src);
const demoTitle = String(title || '').trim() || path.basename(demoAbs);
const code = fs.existsSync(demoAbs) ? fs.readFileSync(demoAbs, 'utf-8') : '';
parts.push(`### ${demoTitle}`);
if (codeAppend) {
parts.push(codeAppend(docFileAbs, src));
}
if (code) {
parts.push(wrapFencedCode(code, getCodeLang(demoAbs)));
}
return `${parts.join('\n\n')}\n`;
} catch {
return full;
}
});
}
/**
* 从 components/version 目录读取 token 数据
* 支持懒加载和缓存,避免重复读取文件
*
* @param api - Dumi API 实例,用于获取项目路径
* @returns token 元数据和数据对象,如果文件不存在则返回 null
*/
function loadTokenFromRepo(api: IApi) {
if (TOKEN_CACHE !== undefined) {
return TOKEN_CACHE;
}
const cwd = api.paths.cwd;
const metaPath = path.join(cwd, 'components', 'version', 'token-meta.json');
const dataPath = path.join(cwd, 'components', 'version', 'token.json');
const meta = readJsonIfExists<TokenMeta>(metaPath);
const data = readJsonIfExists<TokenData>(dataPath);
if (meta && data) {
TOKEN_CACHE = { meta, data };
} else {
TOKEN_CACHE = null;
}
return TOKEN_CACHE;
}
/**
* 转义 markdown 表格单元格中的特殊字符
* 将换行符替换为空格,转义管道符,避免破坏表格结构
*
* @param v - 待转义的值
* @returns 转义后的字符串
*/
function escapeMdCell(v: unknown) {
return String(v ?? '')
.replace(/\r?\n/g, ' ')
.replace(/\|/g, '\\|')
.trim();
}
/**
* 规范化 token 值为字符串格式
* 用于在 markdown 表格中显示 token 的默认值
*
* @param v - 待规范化的值
* @returns 规范化后的字符串null/undefined 返回空字符串
*/
function normalizeValue(v: unknown) {
if (v === undefined || v === null) {
return '';
}
if (typeof v === 'string') {
return v.trim();
}
if (typeof v === 'number' || typeof v === 'boolean') {
return String(v);
}
try {
return JSON.stringify(v);
} catch {
return String(v);
}
}
/**
* 替换 <ComponentTokenTable component="Button" /> 标签为 markdown 表格
* 处理流程:
* 1. 为每个组件生成组件 Token 表格(包含 token 名称、描述、类型、默认值)
* 2. 收集所有组件的全局 Token合并去重后生成全局 Token 表格
* 3. 支持多组件,用逗号分隔(如 component="Button,Input"
* 4. 原位替换原标签,保持文档顺序
*
* @param md - 原始 markdown 内容
* @param context - 内容过滤器上下文信息
* @returns 替换后的 markdown 内容
*/
function replaceComponentTokenTable(md: string, context: ContentFilterContext) {
const tokens = loadTokenFromRepo(context.api);
if (!tokens) {
return md;
}
const { meta: tokenMeta, data: tokenData } = tokens;
const locale = detectDocLocale(context.file);
const labels =
locale === 'zh-CN'
? {
componentTitle: '组件 Token',
globalTitle: '全局 Token',
name: 'Token 名称',
desc: '描述',
type: '类型',
value: '默认值',
}
: {
componentTitle: 'Component Token',
globalTitle: 'Global Token',
name: 'Token Name',
desc: 'Description',
type: 'Type',
value: 'Default Value',
};
const re =
/<ComponentTokenTable\s+[^>]*component="([^"]+)"[^>]*(?:\/>|>(?:\s*<\/ComponentTokenTable>)?)/g;
return md.replace(re, (full, componentProp) => {
const comp = String(componentProp || '').trim();
if (!comp) {
return full;
}
const comps = comp
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const out: string[] = [];
// 使用 Set 收集所有组件的全局 Token自动去重
const globalTokenSet = new Set<string>();
// 遍历每个组件,分别处理组件 Token 和收集全局 Token
comps.forEach((comp) => {
// 1. 处理组件 Token为每个组件生成独立的表格
const metaList = tokenMeta.components?.[comp];
if (Array.isArray(metaList) && metaList.length > 0) {
out.push(`## ${labels.componentTitle} (${comp})`);
out.push(`| ${labels.name} | ${labels.desc} | ${labels.type} | ${labels.value} |`);
out.push(`| --- | --- | --- | --- |`);
metaList.forEach((item) => {
const name = item.token;
// 根据文档语言选择对应的描述文本
const desc = locale === 'zh-CN' ? (item.desc ?? '') : (item.descEn ?? item.desc ?? '');
const type = item.type ?? '';
// 从 tokenData 中获取组件 Token 的默认值
const value = normalizeValue(tokenData?.[comp]?.component?.[name]);
out.push(
`| ${escapeMdCell(name)} | ${escapeMdCell(desc)} | ${escapeMdCell(type)} | ${escapeMdCell(
value,
)} |`,
);
});
out.push('');
}
// 2. 收集全局 Token从每个组件的 global 数组中收集 token 名称
const globalTokens = tokenData?.[comp]?.global;
if (Array.isArray(globalTokens)) {
globalTokens.forEach((token) => {
globalTokenSet.add(token);
});
}
});
// 3. 处理全局 Token合并所有组件的全局 Token生成统一的表格
if (globalTokenSet.size > 0) {
const globalTokenList = Array.from(globalTokenSet).sort();
if (globalTokenList.length > 0) {
out.push(`## ${labels.globalTitle}`);
out.push(`| ${labels.name} | ${labels.desc} | ${labels.type} | ${labels.value} |`);
out.push(`| --- | --- | --- | --- |`);
globalTokenList.forEach((tokenName) => {
const meta = tokenMeta.global?.[tokenName];
if (meta) {
// 根据文档语言选择对应的描述文本
const desc = locale === 'zh-CN' ? (meta.desc ?? '') : (meta.descEn ?? meta.desc ?? '');
const type = meta.type ?? '';
// 全局 Token 的默认值需要在运行时通过 getDesignToken() 计算
// 在静态 markdown 生成阶段无法获取,因此留空
const value = '';
out.push(
`| ${escapeMdCell(tokenName)} | ${escapeMdCell(desc)} | ${escapeMdCell(type)} | ${escapeMdCell(
value,
)} |`,
);
}
});
out.push('');
}
}
// 如果没有生成任何内容,则保留原标签
if (!out.length) {
return full;
}
// 返回生成的 markdown 表格,前后添加换行确保格式正确
return `\n\n${out.join('\n').trim()}\n\n`;
});
}
/**
* 输出处理后的 raw markdown 文件到构建输出目录
* 仅在 production 环境下执行,确保每个文档只处理一次。
* 对收集的路由进行处理:移除 prettier-ignore 注释、替换 `<code src>` 标签、
* 替换 `<ComponentTokenTable />` 组件,然后将处理后的 markdown 写入输出目录
*
* @param api - Dumi API 实例,用于获取输出路径等配置
*/
function emitRawMd(api: IApi) {
if (process.env.NODE_ENV !== 'production') {
return;
}
if (RAW_MD_EMITTED) {
return;
}
RAW_MD_EMITTED = true;
const outRoot = api.paths.absOutputPath;
RAW_MD_ROUTES.forEach((route) => {
try {
const { absPath, file } = route;
const relPath = absPath.replace(/^\//, '');
if (!relPath || !fs.existsSync(file)) {
return;
}
// 应用路由过滤器
if (PLUGIN_OPTIONS.routeFilter && !PLUGIN_OPTIONS.routeFilter(route)) {
return;
}
let content = fs.readFileSync(file, 'utf-8');
const filterContext: ContentFilterContext = {
route,
file,
absPath,
relPath,
api,
};
if (PLUGIN_OPTIONS.contentReplacers && PLUGIN_OPTIONS.contentReplacers.length > 0) {
for (const replacer of PLUGIN_OPTIONS.contentReplacers) {
const replacedContent = replacer(content, filterContext);
if (replacedContent === null || replacedContent === '') {
return;
}
content = replacedContent;
}
}
// 处理步骤:
// 1. 替换 Semantic DOM 部分为指向生成的 semantic.md 文件的链接
content = replaceSemanticDomSection(content, filterContext);
// 2. 替换 <ComponentTokenTable /> 组件为 markdown 表格
content = replaceComponentTokenTable(content, filterContext);
// 3. 替换 prettier-ignore 注释(可通过配置开关控制)
content = replacePrettierIgnore(content);
// 4. 替换 <code src> 标签为完整的代码块(可通过配置开关控制)
if (PLUGIN_OPTIONS.enableReplaceCodeSrc !== false) {
content = replaceCodeSrcToMarkdown(content, file, PLUGIN_OPTIONS.codeAppend);
}
const outMd = path.join(outRoot, `${relPath}.md`);
fs.mkdirSync(path.dirname(outMd), { recursive: true });
fs.writeFileSync(outMd, content, 'utf-8');
api.logger.event(`Build ${relPath}.md`);
} catch (e) {
api.logger.error(`Failed to emit raw markdown for ${route.file}:`, e);
}
});
}
/**
* 负责在构建过程中处理 markdown 文档,生成 flattened markdown 文件到输出目录。
* 主要功能包括:
* 1. 收集所有 markdown 路由
* 2. 在 HTML 文件导出阶段输出处理后的 raw markdown 文件
*
* @param api - Dumi API 实例
*/
export default function rawMdPlugin(api: IApi) {
// 注册配置键,允许用户在配置中使用 rawMd 键
api.describe({
key: 'rawMd',
config: {
schema(joi) {
return joi.object({
routeFilter: joi.function(),
contentReplacers: joi.array().items(joi.function()),
enableReplaceCodeSrc: joi.boolean(),
codeAppend: joi.function(),
});
},
onChange: api.ConfigChangeType.reload,
},
});
const configOptions = api.userConfig.rawMd as PluginOptions | undefined;
PLUGIN_OPTIONS = configOptions || {
enableReplaceCodeSrc: true,
codeAppend: antdCodeAppend,
};
api.modifyRoutes((routes) => {
RAW_MD_ROUTES = Object.values(routes)
.filter((r) => typeof r?.file === 'string' && r.file.endsWith('.md'))
.filter((r) => typeof r?.absPath === 'string' && r.absPath && !r.absPath.includes(':'))
.map((r) => ({ absPath: r.absPath, file: r.file as string }));
return routes;
});
api.modifyExportHTMLFiles((files) => {
emitRawMd(api);
return files;
});
}

View File

@@ -1,50 +0,0 @@
import type { IApi, IRoute } from 'dumi';
const resolve = (p: string): string => require.resolve(p);
export default function routesPlugin(api: IApi) {
api.modifyRoutes((routes) => {
// TODO: append extra routes, such as home, changelog, form-v3
/**
* **important!** Make sure that the `id` and `path` are consistent.
* see: https://github.com/ant-design/ant-design/issues/55960
*/
const extraRoutesList: IRoute[] = [
{
id: 'changelog',
path: 'changelog',
absPath: '/changelog',
parentId: 'DocLayout',
file: resolve('../../../CHANGELOG.en-US.md'),
},
{
id: 'changelog-cn',
path: 'changelog-cn',
absPath: '/changelog-cn',
parentId: 'DocLayout',
file: resolve('../../../CHANGELOG.zh-CN.md'),
},
{
id: 'components/changelog',
path: 'components/changelog',
absPath: '/components/changelog',
parentId: 'DocLayout',
file: resolve('../../../CHANGELOG.en-US.md'),
},
{
id: 'components/changelog-cn',
path: 'components/changelog-cn',
absPath: '/components/changelog-cn',
parentId: 'DocLayout',
file: resolve('../../../CHANGELOG.zh-CN.md'),
},
];
extraRoutesList.forEach((itemRoute) => {
routes[itemRoute.path] = itemRoute;
});
return routes;
});
}

View File

@@ -1,596 +0,0 @@
import fs from 'fs';
import path from 'path';
import type { IApi } from 'dumi';
let COMPONENT_ROUTES: Array<{ absPath: string; componentName: string; outputPath: string }> = [];
let SEMANTIC_MD_EMITTED = false;
// 解析 locales 字符串片段为语义映射表
function extractSemantics(objContent: string): Record<string, string> {
const result: Record<string, string> = {};
const semanticMatches = objContent.matchAll(/['"]?([^'":\s]+)['"]?\s*:\s*['"]([^'"]+)['"],?/g);
for (const match of semanticMatches) {
const [, key, value] = match;
if (key && value) {
result[key] = value;
}
}
return result;
}
/**
* 从 _semantic*.tsx 文件内容中提取语义信息
* @param content - _semantic*.tsx 文件的文件内容字符串
* @returns 包含中文和英文语义描述的对象,失败返回 null
*/
function extractLocaleInfoFromContent(content: string): {
cn: Record<string, string>;
en: Record<string, string>;
} | null {
// 匹配 locales 对象定义
const localesMatch = content.match(/const locales = \{([\s\S]*?)\};/);
if (!localesMatch) {
return null;
}
// 提取中文和英文的语义描述
const cnMatch = content.match(/cn:\s*\{([\s\S]*?)\},?\s*en:/);
if (!cnMatch) {
return null;
}
const enMatch = content.match(/en:\s*\{([\s\S]*?)\}\s*[,;]/);
if (!enMatch) {
return null;
}
const cnContent = cnMatch[1];
const enContent = enMatch[1];
const cnSemantics = extractSemantics(cnContent);
const enSemantics = extractSemantics(enContent);
if (Object.keys(cnSemantics).length === 0) {
return null;
}
return { cn: cnSemantics, en: enSemantics };
}
// 根据 import 路径找到模板文件的实际路径
function resolveTemplateFilePath(semanticFile: string, importPath: string): string | null {
const basePath = path.resolve(path.dirname(semanticFile), importPath);
const candidates = [
basePath,
`${basePath}.tsx`,
`${basePath}.ts`,
path.join(basePath, 'index.tsx'),
path.join(basePath, 'index.ts'),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
// 识别被 JSX 使用的模板组件导入
function parseTemplateUsage(content: string): Array<{ componentName: string; importPath: string }> {
const results: Array<{ componentName: string; importPath: string }> = [];
const importRegex = /import\s+([^;]+?)\s+from\s+['"]([^'"]+)['"];?/g;
for (const match of content.matchAll(importRegex)) {
const importClause = match[1].trim();
const importPath = match[2].trim();
if (!importPath.startsWith('.')) {
continue;
}
const componentNames: string[] = [];
if (importClause.startsWith('{')) {
const names = importClause
.replace(/[{}]/g, '')
.split(',')
.map((name) =>
name
.trim()
.split(/\s+as\s+/)[0]
.trim(),
)
.filter(Boolean);
componentNames.push(...names);
} else if (importClause.includes('{')) {
const [defaultName, namedPart] = importClause.split(',');
if (defaultName?.trim()) {
componentNames.push(defaultName.trim());
}
const names = namedPart
.replace(/[{}]/g, '')
.split(',')
.map((name) =>
name
.trim()
.split(/\s+as\s+/)[0]
.trim(),
)
.filter(Boolean);
componentNames.push(...names);
} else {
componentNames.push(importClause);
}
for (const componentName of componentNames) {
if (new RegExp(`<${componentName}\\b`).test(content)) {
results.push({ componentName, importPath });
}
}
}
return results;
}
// 解析 ignoreSemantics 属性值
function parseIgnoreSemantics(propsString: string): string[] {
const ignoreMatch = propsString.match(/ignoreSemantics\s*=\s*\{([\s\S]*?)\}/);
if (!ignoreMatch) {
return [];
}
const ignoreContent = ignoreMatch[1];
return Array.from(ignoreContent.matchAll(/['"]([^'"]+)['"]/g)).map((match) => match[1]);
}
// 解析 singleOnly 属性值
function parseSingleOnly(propsString: string): boolean {
const singleOnlyMatch = propsString.match(/singleOnly(\s*=\s*\{?([^}\s]+)\}?)?/);
if (!singleOnlyMatch) {
return false;
}
if (!singleOnlyMatch[1]) {
return true;
}
const value = singleOnlyMatch[2];
return value !== 'false';
}
// 抽取模板组件 JSX 的属性字符串
function extractTemplateProps(content: string, componentName: string): string {
const start = content.indexOf(`<${componentName}`);
if (start === -1) {
return '';
}
let index = start + componentName.length + 1;
const propsStart = index;
let braceDepth = 0;
let stringChar: string | null = null;
for (; index < content.length; index += 1) {
const ch = content[index];
const prev = content[index - 1];
if (stringChar) {
if (ch === stringChar && prev !== '\\') {
stringChar = null;
}
continue;
}
if (ch === '"' || ch === "'" || ch === '`') {
stringChar = ch;
continue;
}
if (ch === '{') {
braceDepth += 1;
continue;
}
if (ch === '}') {
if (braceDepth > 0) {
braceDepth -= 1;
}
continue;
}
if (ch === '>' && braceDepth === 0) {
return content.slice(propsStart, index).trim();
}
}
return '';
}
// 应用 ignoreSemantics 与 singleOnly 过滤规则
function filterSemantics(
semantics: Record<string, string>,
options: { ignoreSemantics: string[]; singleOnly: boolean; templatePath: string },
): Record<string, string> {
const ignoreSet = new Set(options.ignoreSemantics);
const filteredEntries = Object.entries(semantics).filter(([key]) => !ignoreSet.has(key));
if (options.singleOnly) {
const singleOnlyKeys = new Set(['item', 'itemContent', 'itemRemove']);
return Object.fromEntries(filteredEntries.filter(([key]) => !singleOnlyKeys.has(key)));
}
return Object.fromEntries(filteredEntries);
}
// 从模板组件的 locales 提取语义信息
function extractSemanticInfoFromTemplate(
semanticFile: string,
content: string,
): { cn: Record<string, string>; en: Record<string, string> } | null {
const templates = parseTemplateUsage(content);
if (templates.length === 0) {
return null;
}
for (const template of templates) {
const templatePath = resolveTemplateFilePath(semanticFile, template.importPath);
if (!templatePath) {
continue;
}
const templateContent = fs.readFileSync(templatePath, 'utf-8');
const templateLocales = extractLocaleInfoFromContent(templateContent);
if (!templateLocales) {
continue;
}
const propsString = extractTemplateProps(content, template.componentName);
const ignoreSemantics = parseIgnoreSemantics(propsString);
const singleOnly = parseSingleOnly(propsString);
return {
cn: filterSemantics(templateLocales.cn, {
ignoreSemantics,
singleOnly,
templatePath,
}),
en: filterSemantics(templateLocales.en, {
ignoreSemantics,
singleOnly,
templatePath,
}),
};
}
return null;
}
// 从 _semantic*.tsx 中提取语义信息
function extractSemanticInfo(semanticFile: string): {
cn: Record<string, string>;
en: Record<string, string>;
} | null {
try {
if (!fs.existsSync(semanticFile)) {
return null;
}
const content = fs.readFileSync(semanticFile, 'utf-8');
const localeInfo = extractLocaleInfoFromContent(content);
if (localeInfo) {
return localeInfo;
}
return extractSemanticInfoFromTemplate(semanticFile, content);
} catch (error) {
if (process.env.DEBUG) {
console.error(`Failed to extract semantic info from ${semanticFile}:`, error);
}
return null;
}
}
/**
* 生成语义名称列表的 markdown 格式
* @param semantics - 语义信息对象
* @returns markdown 格式的语义名称列表
*/
function generateSemanticParts(semantics: Record<string, string>): string {
const parts: string[] = [];
for (const [name, desc] of Object.entries(semantics)) {
// 将点号替换为连字符,匹配 DOM 中的实际 className 格式
const className = name.replace(/\./g, '-');
parts.push(`- ${name}\`semantic-mark-${className}\`: ${desc}`);
}
return parts.join('\n');
}
/**
* 生成使用案例代码模板
* @param componentName - 组件名(如 "button", "float-button"
* @param semanticSuffix - 语义文件后缀(如 "_group", "_input", "_search"
* @param semantics - 语义信息对象
* @returns 使用案例代码
*/
function generateUsageExample(
componentName: string,
semanticSuffix: string,
semantics: Record<string, string>,
): string {
// 将组件名转换为 PascalCase如 "float-button" -> "FloatButton"
const componentDisplayName = componentName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// 根据后缀生成组件名
let tagName = componentDisplayName;
if (semanticSuffix) {
// 移除开头的下划线,然后转换为 PascalCase
// 如 "_group" -> "Group", "_input" -> "Input", "_search" -> "Search"
const suffixParts = semanticSuffix
.replace(/^_/, '')
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1));
const subComponentName = suffixParts.join('');
tagName = `${componentDisplayName}.${subComponentName}`;
}
// 生成 classNames 对象
const classNamesEntries: string[] = [];
for (const [name] of Object.entries(semantics)) {
// 将点号替换为连字符,匹配 DOM 中的实际 className 格式
const className = name.replace(/\./g, '-');
classNamesEntries.push(` ${name}: "semantic-mark-${className}"`);
}
return `<${tagName}
{...otherProps}
classNames={{
${classNamesEntries.join(',\n')}
}}
/>`;
}
/**
* 从测试快照文件中读取组件的 HTML snapshot并提取包含所有 semantic 元素的最小根元素
* 注意:此函数依赖 Jest 快照文件的路径与命名约定(含 snapshot key 规则)。
* 若测试目录结构或快照格式变化,文档构建可能失败或产生不完整结果,请同步调整此处逻辑。
* @param semanticFile - _semantic*.tsx 文件的绝对路径
* @param cwd - 项目根目录
* @returns HTML 字符串,失败返回 null
*/
function getComponentHTMLSnapshot(semanticFile: string, cwd: string): string | null {
try {
const relativePath = path.relative(cwd, semanticFile);
const pathMatch = relativePath.match(/^components\/([^/]+)\/demo\/([^/]+)\.tsx$/);
if (!pathMatch) {
return null;
}
const [, componentName, fileName] = pathMatch;
const snapshotPath = path.join(
cwd,
'components',
componentName,
'__tests__',
'__snapshots__',
'demo-semantic.test.tsx.snap',
);
if (!fs.existsSync(snapshotPath)) {
return null;
}
const snapshotContent = fs.readFileSync(snapshotPath, 'utf-8');
// 匹配快照 keyexports[`renders components/button/demo/_semantic.tsx correctly 1`] = `...`;
const snapshotKeyPattern = `components/${componentName}/demo/${fileName}.tsx correctly`;
const regex = new RegExp(
`exports\\[\\\`[^\\\`]*${snapshotKeyPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\\`]*\\\`\\]\\s*=\\s*\\\`([\\s\\S]*?)\\\`;`,
);
const snapshotMatch = snapshotContent.match(regex);
if (!snapshotMatch) {
return null;
}
let html = snapshotMatch[1].trim();
// 处理 JSON 格式的快照:{ type: 'demo', html: '...' }
if (html.startsWith('{') && html.includes('"html"')) {
try {
const parsed = JSON.parse(html);
if (parsed.html) {
html = parsed.html;
}
} catch {}
}
const { JSDOM } = require('jsdom');
const dom = new JSDOM(html);
const document = dom.window.document;
const semanticElements = document.querySelectorAll('[class*="semantic-"]');
if (semanticElements.length === 0) {
return html;
}
// 向上查找包含所有 semantic 元素的最小根元素(通常是组件根元素,如 button
const firstSemantic = semanticElements[0] as Element;
let rootElement: Element | null = firstSemantic;
while (rootElement && rootElement.parentElement) {
const parent: Element | null = rootElement.parentElement;
// 遇到布局容器时停止,避免包含 SemanticPreview 的外层容器
if (
!parent ||
parent.classList.contains('ant-row') ||
parent.classList.contains('ant-col') ||
parent.classList.contains('acss-') ||
parent === document.body ||
parent === document.documentElement
) {
break;
}
// 检查父元素是否包含所有 semantic 元素
let parentContainsAll = true;
for (let i = 0; i < semanticElements.length; i++) {
if (!parent.contains(semanticElements[i] as Element)) {
parentContainsAll = false;
break;
}
}
if (parentContainsAll) {
rootElement = parent;
} else {
break;
}
}
return rootElement ? rootElement.outerHTML : html;
} catch (error) {
if (process.env.DEBUG) {
console.warn(`[semantic-md] Failed to get HTML snapshot from ${semanticFile}:`, error);
}
return null;
}
}
/**
* 为每个组件生成 semantic*.md 文件
* @param api - Dumi API 实例
*/
function emitSemanticMd(api: IApi) {
if (process.env.NODE_ENV !== 'production') {
return;
}
if (SEMANTIC_MD_EMITTED) {
return;
}
SEMANTIC_MD_EMITTED = true;
const outRoot = api.paths.absOutputPath;
const cwd = api.paths.cwd;
COMPONENT_ROUTES.forEach(({ componentName, outputPath, absPath }) => {
try {
const componentDir = path.join(cwd, 'components', componentName);
const demoDir = path.join(componentDir, 'demo');
if (fs.existsSync(demoDir)) {
const demoFiles = fs.readdirSync(demoDir);
// 查找所有 _semantic*.tsx 文件(如 _semantic.tsx, _semantic_group.tsx
const semanticFiles = demoFiles.filter(
(demoFile) => demoFile.startsWith('_semantic') && demoFile.endsWith('.tsx'),
);
semanticFiles.forEach((semanticFile) => {
const semanticFilePath = path.join(demoDir, semanticFile);
const semanticInfo = extractSemanticInfo(semanticFilePath);
if (!semanticInfo) {
if (process.env.DEBUG) {
console.warn(
`[semantic-md] Failed to extract semantic info from ${semanticFilePath}`,
);
}
}
// 生成对应的 markdown 文件名_semantic.tsx -> semantic.md, _semantic_group.tsx -> semantic_group.md
const semanticSuffix = semanticFile.replace(/^_semantic/, '').replace(/\.tsx$/, '');
const semanticMdFileName = `semantic${semanticSuffix}.md`;
const semanticMdPath = path.join(outRoot, outputPath, semanticMdFileName);
const semanticMdDir = path.dirname(semanticMdPath);
if (!fs.existsSync(semanticMdDir)) {
fs.mkdirSync(semanticMdDir, { recursive: true });
}
// 根据路由路径判断语言(-cn 后缀表示中文)
const isZhCN = absPath.includes('-cn');
let semantics: Record<string, string> = {};
if (semanticInfo) {
semantics = isZhCN ? semanticInfo.cn : semanticInfo.en;
}
const displayName = componentName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const titleSuffix = semanticSuffix
? semanticSuffix
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('.')
: '';
const parts = [
`## ${displayName}${titleSuffix ? `${titleSuffix}` : ''}`,
'',
'### Semantic Parts',
'',
];
if (Object.keys(semantics).length > 0) {
parts.push(generateSemanticParts(semantics));
parts.push('');
parts.push(`### ${isZhCN ? '使用案例' : 'Usage Example'}`);
parts.push('');
parts.push('```tsx');
parts.push(generateUsageExample(componentName, semanticSuffix, semantics));
parts.push('```');
parts.push('');
parts.push('### Abstract DOM Structure');
parts.push('');
const htmlSnapshot = semanticInfo
? getComponentHTMLSnapshot(semanticFilePath, cwd)
: null;
if (htmlSnapshot) {
parts.push('```html');
parts.push(htmlSnapshot);
parts.push('```');
}
}
const content = `${parts.join('\n')}\n`;
fs.writeFileSync(semanticMdPath, content, 'utf-8');
});
}
} catch (e) {
api.logger.error(`Failed to generate semantic md for ${componentName}:`, e);
}
});
}
/**
* Semantic markdown 生成插件
* @param api - Dumi API 实例
*/
export default function semanticMdPlugin(api: IApi) {
// 收集组件路由信息(过滤出 /components/* 和 /components/*-cn 路由)
api.modifyRoutes((routes) => {
COMPONENT_ROUTES = Object.values(routes)
.filter((r) => typeof r?.absPath === 'string' && r.absPath && !r.absPath.includes(':'))
.filter((r) => {
const match = r.absPath.match(/^\/components\/([^/]+)\/?$/);
return !!match;
})
.map((r) => {
const match = r.absPath.match(/^\/components\/([^/]+)\/?$/);
const fullComponentName = match![1];
// 移除 -cn 后缀获取基础组件名
const baseComponentName = fullComponentName.replace(/-cn$/, '');
const outputPath = `components/${fullComponentName}`;
return {
absPath: r.absPath,
componentName: baseComponentName,
outputPath,
};
});
return routes;
});
// 在 HTML 文件导出阶段生成 semantic.md 文件
api.modifyExportHTMLFiles((files) => {
emitSemanticMd(api);
return files;
});
}

View File

@@ -1,101 +0,0 @@
import fs from 'fs';
import type { IApi } from 'dumi';
import ReactTechStack from 'dumi/dist/techStacks/react';
import tsToJs from '../utils/tsToJs';
import { dependencies, devDependencies } from '../../../package.json';
/**
* extends dumi internal tech stack, for customize previewer props
*/
class AntdReactTechStack extends ReactTechStack {
generatePreviewerProps(...[props, opts]: any) {
props.pkgDependencyList = { ...devDependencies, ...dependencies };
props.jsx ??= '';
if (opts.type === 'code-block') {
props.jsx = opts?.entryPointCode ? tsToJs(opts.entryPointCode) : '';
}
if (opts.type === 'external') {
// try to find md file with the same name as the demo tsx file
const locale = opts.mdAbsPath.match(/index\.([a-z-]+)\.md$/i)?.[1];
const mdPath = opts.fileAbsPath!.replace(/\.\w+$/, '.md');
const md = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : '';
const codePath = opts.fileAbsPath!.replace(/\.\w+$/, '.tsx');
const code = fs.existsSync(codePath) ? fs.readFileSync(codePath, 'utf-8') : '';
props.jsx = tsToJs(code);
if (md) {
// extract description & css style from md file
const blocks: Record<string, string> = {};
const lines = md.split('\n');
let blockName = '';
let cacheList: string[] = [];
// Get block name
const getBlockName = (text: string) => {
if (text.startsWith('## ')) {
return text.replace('## ', '').trim();
}
if (text.startsWith('```css') || text.startsWith('<style>')) {
return 'style';
}
return null;
};
// Fill block content
const fillBlock = (name: string, lineList: string[]) => {
if (lineList.length) {
let fullText: string;
if (name === 'style') {
fullText = lineList
.join('\n')
.replace(/<\/?style>/g, '')
.replace(/```(\s*css)/g, '');
} else {
fullText = lineList.slice(1).join('\n');
}
blocks[name] = fullText;
}
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Mark as new block
const nextBlockName = getBlockName(line);
if (nextBlockName) {
fillBlock(blockName, cacheList);
// Next Block
blockName = nextBlockName;
cacheList = [line];
} else {
cacheList.push(line);
}
}
// Last block
fillBlock(blockName, cacheList);
props.description = blocks[locale];
props.style = blocks.style;
}
}
return props;
}
}
export default function techStackPlugin(api: IApi) {
api.registerTechStack(() => new AntdReactTechStack());
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { RightOutlined, YuqueOutlined, ZhihuOutlined } from '@ant-design/icons';
import { Button, Card, Divider } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { clsx } from 'clsx';
import useLocale from '../../../hooks/useLocale';
@@ -10,7 +10,7 @@ import JuejinIcon from '../../../theme/icons/JuejinIcon';
const ANTD_IMG_URL =
'https://picx.zhimg.com/v2-3b2bca09c2771e7a82a81562e806be4d.jpg?source=d16d100b';
const styles = createStaticStyles(({ cssVar, css }) => ({
const useStyle = createStyles(({ cssVar, css }) => ({
card: css`
width: 100%;
margin: calc(${cssVar.marginMD} * 2) 0;
@@ -113,17 +113,19 @@ interface Props {
const ColumnCard: React.FC<Props> = ({ zhihuLink, yuqueLink, juejinLink }) => {
const [locale] = useLocale(locales);
const {
card,
bigTitle,
cardBody,
leftCard,
title,
subTitle,
logo,
arrowIcon,
zlBtn,
discussLogo,
} = styles;
styles: {
card,
bigTitle,
cardBody,
leftCard,
title,
subTitle,
logo,
arrowIcon,
zlBtn,
discussLogo,
},
} = useStyle();
if (!zhihuLink && !yuqueLink && !juejinLink) {
return null;
}

View File

@@ -1,13 +1,13 @@
import React, { Suspense } from 'react';
import ContributorsList from '@qixian.cs/github-contributors-list';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { clsx } from 'clsx';
import { useIntl } from 'dumi';
import SiteContext from '../SiteContext';
import ContributorAvatar from './ContributorAvatar';
const styles = createStaticStyles(({ cssVar, css }) => ({
const useStyle = createStyles(({ cssVar, css }) => ({
listMobile: css`
margin: 1em 0 !important;
`,
@@ -48,6 +48,7 @@ const blockList = [
const Contributors: React.FC<ContributorsProps> = ({ filename }) => {
const { formatMessage } = useIntl();
const { styles } = useStyle();
const { isMobile } = React.use(SiteContext);
if (!filename) {

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { removeCSS, updateCSS } from '@rc-component/util/lib/Dom/dynamicCSS';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import useLocale from '../../../hooks/useLocale';
@@ -20,7 +20,7 @@ const locales = {
},
};
const styles = createStaticStyles(({ css, cssVar }) => ({
const useStyle = createStyles(({ css, cssVar }) => ({
container: css`
position: fixed;
inset-inline-start: 0;
@@ -80,6 +80,8 @@ const InfoNewVersion: React.FC = () => {
removeCSS(whereCls);
}, []);
const { styles } = useStyle();
if (supportWhere) {
return null;
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { omit } from '@rc-component/util';
import { Button, Tooltip } from 'antd';
import { createStaticStyles } from 'antd-style';
import { createStyles } from 'antd-style';
import { clsx } from 'clsx';
export interface SwitchBtnProps {
@@ -18,7 +18,7 @@ export interface SwitchBtnProps {
const BASE_SIZE = '1.2em';
const styles = createStaticStyles(({ cssVar, css }) => {
const useStyle = createStyles(({ cssVar, css }) => {
return {
btn: css`
width: ${cssVar.controlHeight};
@@ -65,7 +65,9 @@ const styles = createStaticStyles(({ cssVar, css }) => {
const SwitchBtn: React.FC<SwitchBtnProps> = (props) => {
const { label1, label2, tooltip1, tooltip2, value, pure, onClick, ...rest } = props;
const { btn, innerDiv, labelStyle, label1Style, label2Style } = styles;
const {
styles: { btn, innerDiv, labelStyle, label1Style, label2Style },
} = useStyle();
const node = (
<Button

View File

@@ -2,14 +2,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { GithubOutlined, MenuOutlined } from '@ant-design/icons';
import { Alert, Button, Col, ConfigProvider, Popover, Row, Select, Tooltip } from 'antd';
import { createStyles } from 'antd-style';
import type { DefaultOptionType } from 'antd/es/select';
import { clsx } from 'clsx';
import dayjs from 'dayjs';
import { useLocation, useSiteData } from 'dumi';
import DumiSearchBar from 'dumi/theme-default/slots/SearchBar';
import useSWR from 'swr';
import versionsFile from '../../../../public/versions.json';
import useLocale from '../../../hooks/useLocale';
import useLocalStorage from '../../../hooks/useLocalStorage';
import { getBannerData } from '../../../pages/index/components/util';
@@ -17,6 +14,7 @@ import ThemeSwitch from '../../common/ThemeSwitch';
import DirectionIcon from '../../icons/DirectionIcon';
import { ANT_DESIGN_NOT_SHOW_BANNER } from '../../layouts/GlobalLayout';
import * as utils from '../../utils';
import { getThemeConfig } from '../../utils';
import SiteContext from '../SiteContext';
import type { SharedProps } from './interface';
import Logo from './Logo';
@@ -134,8 +132,7 @@ const useStyle = createStyles(({ cssVar, token, css }) => {
}
`,
versionSelect: css`
width: 112px;
min-width: 112px; // 这个宽度需要和 Empty 状态的宽度保持一致
min-width: 90px;
.rc-virtual-list {
.rc-virtual-list-holder {
scrollbar-width: thin;
@@ -152,56 +149,19 @@ interface HeaderState {
searching: boolean;
}
interface VersionItem {
version: string;
url: string;
chineseMirrorUrl?: string;
}
const fetcher = (...args: Parameters<typeof fetch>) => {
return fetch(...args).then((res) => res.json());
};
// ================================= Header =================================
const Header: React.FC = () => {
const [, lang] = useLocale();
const { pkg } = useSiteData();
const isChineseMirror =
typeof window !== 'undefined' && typeof window.location !== 'undefined'
? window.location.hostname.includes('.antgroup.com')
: false;
const { data: versions = [], isLoading } = useSWR<VersionItem[]>(
process.env.NODE_ENV === 'production' && typeof window !== 'undefined'
? `${window.location.origin}/versions.json`
: null,
fetcher,
{
fallbackData: versionsFile,
errorRetryCount: 3,
},
);
const versionOptions = useMemo(() => {
if (isLoading) {
return [];
}
return versions.map<DefaultOptionType>((item) => {
const isMatch = item.version.startsWith(pkg.version[0]);
const label = isMatch ? pkg.version : item.version;
const value = isChineseMirror && item.chineseMirrorUrl ? item.chineseMirrorUrl : item.url;
return { value, label };
});
}, [versions, isLoading, pkg.version, isChineseMirror]);
const themeConfig = getThemeConfig();
const [headerState, setHeaderState] = useState<HeaderState>({
menuVisible: false,
windowWidth: 1400,
searching: false,
});
const { direction, isMobile, bannerVisible, updateSiteConfig } = React.use(SiteContext);
const pingTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const location = useLocation();
@@ -298,6 +258,14 @@ const Header: React.FC = () => {
);
const { menuVisible, windowWidth, searching } = headerState;
const docVersions: Record<string, string> = {
[pkg.version]: pkg.version,
...themeConfig?.docVersions,
};
const versionOptions = Object.keys(docVersions).map((version) => ({
value: docVersions[version],
label: version,
}));
const isHome = ['', 'index', 'index-cn'].includes(pathname);
const isZhCN = lang === 'cn';
@@ -340,7 +308,6 @@ const Header: React.FC = () => {
key="version"
size="small"
variant="filled"
loading={isLoading}
className={styles.versionSelect}
defaultValue={pkg.version}
onChange={handleVersionChange}

View File

@@ -1,3 +1,6 @@
const chineseMirror =
typeof location !== 'undefined' && location.hostname.includes('.antgroup.com');
export default {
categoryOrder: {
'Ant Design': 0,
@@ -42,4 +45,15 @@ export default {
模板文档: 3,
'Template Document': 3,
},
docVersions: {
'5.x': chineseMirror ? 'https://5x-ant-design.antgroup.com' : 'https://5x.ant.design',
'4.x': chineseMirror ? 'https://4x-ant-design.antgroup.com' : 'https://4x.ant.design',
'3.x': 'https://3x.ant.design',
'2.x': 'https://2x.ant.design',
'1.x': 'https://1x.ant.design',
'0.12.x': 'https://012x.ant.design',
'0.11.x': 'https://011x.ant.design',
'0.10.x': 'https://010x.ant.design',
'0.9.x': 'https://09x.ant.design',
},
};

View File

@@ -3,6 +3,7 @@ import flattenDeep from 'lodash/flattenDeep';
import semver from 'semver';
import deprecatedVersions from '../../../BUG_VERSIONS.json';
import themeConfig from '../themeConfig';
interface Meta {
skip?: boolean;
@@ -214,3 +215,5 @@ export function isOfficialHost(hostname: string) {
const officialHostnames = ['ant.design', 'antgroup.com'];
return officialHostnames.some((official) => hostname.includes(official));
}
export const getThemeConfig = () => themeConfig;

View File

@@ -160,27 +160,18 @@ export default defineConfig({
}
// 首页无视链接里面的语言设置 https://github.com/ant-design/ant-design/issues/4552
const normalizedPathname = pathname || '/';
if (normalizedPathname === '/' || normalizedPathname === '/index-cn') {
let lang;
if (window.localStorage) {
const antLocale = localStorage.getItem('ANT_LOCAL_TYPE_KEY');
// 尝试解析 JSON因为可能是被序列化后存储的 "en-US" / en-US https://github.com/ant-design/ant-design/issues/56606
try {
lang = antLocale ? JSON.parse(antLocale) : localStorage.getItem('locale');
} catch (e) {
lang = antLocale ? antLocale : localStorage.getItem('locale');
}
}
lang = lang || ((navigator.language || navigator.browserLanguage).toLowerCase() === 'zh-cn'
if (pathname === '/' || pathname === '/index-cn') {
const lang =
(window.localStorage && localStorage.getItem('locale')) ||
((navigator.language || navigator.browserLanguage).toLowerCase() === 'zh-cn'
? 'zh-CN'
: 'en-US');
// safari is 'zh-cn', while other browser is 'zh-CN';
if ((lang === 'zh-CN') !== isZhCN(normalizedPathname)) {
location.pathname = getLocalizedPathname(normalizedPathname, lang === 'zh-CN');
if ((lang === 'zh-CN') !== isZhCN(pathname)) {
location.pathname = getLocalizedPathname(pathname, lang === 'zh-CN');
}
}
document.documentElement.className += isZhCN(normalizedPathname) ? 'zh-cn' : 'en-us';
document.documentElement.className += isZhCN(pathname) ? 'zh-cn' : 'en-us';
})();
`,
],

View File

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

View File

@@ -63,7 +63,7 @@ jobs:
steps:
# We need get PR id first
- name: download pr artifact
uses: dawidd6/action-download-artifact@v14
uses: dawidd6/action-download-artifact@v12
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
run_id: ${{ github.event.workflow_run.id }}
@@ -83,7 +83,7 @@ jobs:
# Download site artifact
- name: download site artifact
if: ${{ fromJSON(needs.upstream-workflow-summary.outputs.build-success) }}
uses: dawidd6/action-download-artifact@v14
uses: dawidd6/action-download-artifact@v12
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
run_id: ${{ github.event.workflow_run.id }}

View File

@@ -34,7 +34,6 @@ jobs:
shard: [1/2, 2/2]
env:
REACT: ${{ matrix.react }}
SKIP_SEMANTIC: '1'
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v6

View File

@@ -70,7 +70,7 @@ jobs:
# We need get persist-index first
- name: download image snapshot artifact
uses: dawidd6/action-download-artifact@v14
uses: dawidd6/action-download-artifact@v12
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
run_id: ${{ github.event.workflow_run.id }}
@@ -92,7 +92,7 @@ jobs:
- name: download report artifact
id: download_report
if: ${{ needs.upstream-workflow-summary.outputs.build-status == 'success' || needs.upstream-workflow-summary.outputs.build-status == 'failure' }}
uses: dawidd6/action-download-artifact@v14
uses: dawidd6/action-download-artifact@v12
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
run_id: ${{ github.event.workflow_run.id }}

View File

@@ -67,7 +67,7 @@ jobs:
# We need get persist key first
- name: Download Visual Regression Ref
uses: dawidd6/action-download-artifact@v14
uses: dawidd6/action-download-artifact@v12
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
run_id: ${{ github.event.workflow_run.id }}
@@ -81,7 +81,7 @@ jobs:
- name: Download Visual-Regression Artifact
if: ${{ fromJSON(needs.upstream-workflow-summary.outputs.build-success) }}
uses: dawidd6/action-download-artifact@v14
uses: dawidd6/action-download-artifact@v12
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
run_id: ${{ github.event.workflow_run.id }}

1
.gitignore vendored
View File

@@ -78,7 +78,6 @@ __image_snapshots__/
.env
examples/
.neocoder/
.claude/
# generated css
components/style/antd.css

View File

@@ -1,11 +1,13 @@
const { moduleNameMapper, transformIgnorePatterns } = require('./.jest');
// jest config for image snapshots
module.exports = {
setupFiles: ['./tests/setup.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'md'],
moduleNameMapper,
transform: {
'^.+\\.(ts|tsx|js|mjs)$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.tsx?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.js$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.md$': './node_modules/@ant-design/tools/lib/jest/demoPreprocessor',
'\\.(jpg|png|gif|svg)$': './node_modules/@ant-design/tools/lib/jest/imagePreprocessor',
},

View File

@@ -11,7 +11,6 @@ const compileModules = [
'parse5',
'@exodus',
'jsdom',
'@csstools',
];
// cnpm use `_` as prefix
@@ -35,10 +34,6 @@ function getTestRegex(libDir) {
return '.*\\.test\\.(j|t)sx?$';
}
const shouldIgnoreSemantic =
['dist', 'lib', 'es', 'dist-min'].includes(process.env.LIB_DIR) ||
['1', 'true'].includes(process.env.SKIP_SEMANTIC);
module.exports = {
verbose: true,
testEnvironment: 'jsdom',
@@ -53,17 +48,10 @@ module.exports = {
'^antd/lib/(.*)$': '<rootDir>/components/$1',
'^antd/locale/(.*)$': '<rootDir>/components/locale/$1',
},
testPathIgnorePatterns: [
'/node_modules/',
'dekko',
'node',
'image.test.js',
'image.test.ts',
...(shouldIgnoreSemantic ? ['demo-semantic.test'] : []),
],
testPathIgnorePatterns: ['/node_modules/', 'dekko', 'node', 'image.test.js', 'image.test.ts'],
transform: {
'\\.tsx?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.(m?)js(m)?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.(m?)js$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.md$': './node_modules/@ant-design/tools/lib/jest/demoPreprocessor',
'\\.(jpg|png|gif|svg)$': './node_modules/@ant-design/tools/lib/jest/imagePreprocessor',
},
@@ -76,7 +64,6 @@ module.exports = {
'!components/*/__tests__/type.test.tsx',
'!components/**/*/interface.{ts,tsx}',
'!components/*/__tests__/image.test.{ts,tsx}',
'!components/*/__tests__/demo-semantic.test.tsx',
'!components/__tests__/node.test.tsx',
'!components/*/demo/*.tsx',
'!components/*/design/**',

View File

@@ -1,12 +1,14 @@
const { moduleNameMapper, transformIgnorePatterns } = require('./.jest');
// jest config for server render environment
module.exports = {
setupFiles: ['./tests/setup.ts'],
setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'md'],
moduleNameMapper,
transform: {
'^.+\\.(ts|tsx|js|mjs)$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.tsx?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.js$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor',
'\\.md$': './node_modules/@ant-design/tools/lib/jest/demoPreprocessor',
'\\.(jpg|png|gif|svg)$': './node_modules/@ant-design/tools/lib/jest/imagePreprocessor',
},

1231
AGENTS.md

File diff suppressed because it is too large Load Diff

View File

@@ -15,118 +15,6 @@ tag: vVERSION
---
## 6.3.0
`2026-02-10`
- ConfigProvider
- 🆕 Support ConfigProvider global configuration of `maskClosable` for Modal and Drawer. [#56739](https://github.com/ant-design/ant-design/pull/56739) [@luozz1994](https://github.com/luozz1994)
- 🆕 Support ConfigProvider `suffixIcon` global configuration for DatePicker and TimePicker. [#56709](https://github.com/ant-design/ant-design/pull/56709) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Support ConfigProvider `expandIcon` and `loadingIcon` global configuration for Cascader. [#56482](https://github.com/ant-design/ant-design/pull/56482) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Support ConfigProvider `scroll` global configuration for Table. [#56628](https://github.com/ant-design/ant-design/pull/56628) [@Clayton](https://github.com/Clayton)
- 🆕 Support ConfigProvider `className` and `style` configuration for App, and `arrow` prop for ColorPicker. [#56573](https://github.com/ant-design/ant-design/pull/56573) [@zombieJ](https://github.com/zombieJ)
- 🆕 Support ConfigProvider `loadingIcon` global configuration for Button. [#56439](https://github.com/ant-design/ant-design/pull/56439) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Support ConfigProvider `rangePicker.separator` global configuration. [#56499](https://github.com/ant-design/ant-design/pull/56499) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Support ConfigProvider `tooltipIcon` and `tooltipProps` global configuration for Form. [#56372](https://github.com/ant-design/ant-design/pull/56372) [@guoyunhe](https://github.com/guoyunhe)
- Upload
- 🆕 Add Upload `classNames.trigger` and `styles.trigger` props. [#56578](https://github.com/ant-design/ant-design/pull/56578) [@QdabuliuQ](https://github.com/QdabuliuQ)
- 🆕 Support Upload.Dragger `onDoubleClick` event. [#56579](https://github.com/ant-design/ant-design/pull/56579) [@ug-hero](https://github.com/ug-hero)
- 🐞 Fix Upload missing default height for `picture-card` / `picture-circle` parent nodes. [#56864](https://github.com/ant-design/ant-design/pull/56864) [@wanpan11](https://github.com/wanpan11)
- 🆕 Add Grid `xxxl` (1920px) breakpoint to adapt to FHD screens. [#56825](https://github.com/ant-design/ant-design/pull/56825) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Support Switch `indicator` customization for semantic structure. [#56710](https://github.com/ant-design/ant-design/pull/56710) [@zombieJ](https://github.com/zombieJ)
- Button
- 🐞 Fix Button reversed `hover` and `active` colors for `color` in dark theme. [#56872](https://github.com/ant-design/ant-design/pull/56872) [@zombieJ](https://github.com/zombieJ)
- 🐞 Fix Button border size not following Design Token `lineWidth`. [#56683](https://github.com/ant-design/ant-design/pull/56683) [@zombieJ](https://github.com/zombieJ)
- Select
- 💄 Remove Select redundant `-content-value` div DOM in single mode to optimize semantic structure, allowing override via `classNames` and `styles`. [#56811](https://github.com/ant-design/ant-design/pull/56811) [@zombieJ](https://github.com/zombieJ)
- 🐞 Fix Select `notFoundContent` not taking effect. [#56756](https://github.com/ant-design/ant-design/pull/56756) [@QdabuliuQ](https://github.com/QdabuliuQ)
- Radio
- 🐞 Fix Radio.Group extra right margin for radio items when vertically aligned. [#56909](https://github.com/ant-design/ant-design/pull/56909) [@jany55555](https://github.com/jany55555)
- 💄 Remove Radio `-inner` DOM node of `icon` sub-element for better semantic structure adaptation. [#56783](https://github.com/ant-design/ant-design/pull/56783) [@zombieJ](https://github.com/zombieJ)
- 💄 Disable Modal & Drawer mask blur effect by default. [#56781](https://github.com/ant-design/ant-design/pull/56781) [@aojunhao123](https://github.com/aojunhao123)
- 🐞 Fix Tooltip & Popover popup animation starting position being shifted to the left. [#56887](https://github.com/ant-design/ant-design/pull/56887) [@zombieJ](https://github.com/zombieJ)
- 🐞 Fix List color-related tokens not working for deprecated component config. [#56913](https://github.com/ant-design/ant-design/pull/56913) [@zombieJ](https://github.com/zombieJ)
- 🛠 Refactor Spin DOM structure to align across different scenarios and support full Semantic Structure. [#56852](https://github.com/ant-design/ant-design/pull/56852) [@zombieJ](https://github.com/zombieJ)
- ⌨️ ♿ Add Icon accessibility names to the search icon SVG to improve screen reader support. [#56521](https://github.com/ant-design/ant-design/pull/56521) [@huangkevin-apr](https://github.com/huangkevin-apr)
- 🐞 Fix Cascader filter list resetting immediately when closing on selection in search mode, affecting UX. [#56764](https://github.com/ant-design/ant-design/pull/56764) [@zombieJ](https://github.com/zombieJ)
- ⌨️ ♿ Improve Tree accessibility support. [#56716](https://github.com/ant-design/ant-design/pull/56716) [@aojunhao123](https://github.com/aojunhao123)
- 🐞 Support ColorPicker semantic structure for selection block, and fix `root` semantic being incorrectly applied to popup elements. [#56607](https://github.com/ant-design/ant-design/pull/56607) [@zombieJ](https://github.com/zombieJ)
- 💄 Change Avatar default value of `size` from `default` to `medium` for consistency. [#56440](https://github.com/ant-design/ant-design/pull/56440) [@guoyunhe](https://github.com/guoyunhe)
- 💄 Remove Checkbox `-inner` DOM node of `icon` sub-element for better semantic structure adaptation. [#56783](https://github.com/ant-design/ant-design/pull/56783) [@zombieJ](https://github.com/zombieJ)
- MISC
- 🐞 MISC: Fix React Compiler compatibility in UMD version, now disabled by default. [#56830](https://github.com/ant-design/ant-design/pull/56830) [@zombieJ](https://github.com/zombieJ)
- 🛠 Streamline `styles` and `classNames` type definitions for better standardization. [#56758](https://github.com/ant-design/ant-design/pull/56758) [@crazyair](https://github.com/crazyair)
## 6.2.3
`2026-02-02`
- Button
- 🐞 Fix Button `defaultBg`, `defaultColor`, `defaultHoverColor` and `defaultActiveColor` tokens not taking effect. [#56238](https://github.com/ant-design/ant-design/pull/56238) [@ug-hero](https://github.com/ug-hero)
- 🐞 Fix Button default tokens not taking effect. [#56719](https://github.com/ant-design/ant-design/pull/56719) [@unknowntocka](https://github.com/unknowntocka)
- 🐞 Fix Button `variant="solid"` borders displaying incorrectly inside Space.Compact. [#56486](https://github.com/ant-design/ant-design/pull/56486) [@Pareder](https://github.com/Pareder)
- 🐞 Fix Input.TextArea ref missing `nativeElement` property. [#56803](https://github.com/ant-design/ant-design/pull/56803) [@smith3816](https://github.com/smith3816)
- 🐞 Fix Flex default `align` not taking effect when using `orientation`. [#55950](https://github.com/ant-design/ant-design/pull/55950) [@YingtaoMo](https://github.com/YingtaoMo)
- 🐞 Fix Typography link selector specificity being too low causing styles to be overridden. [#56759](https://github.com/ant-design/ant-design/pull/56759) [@QDyanbing](https://github.com/QDyanbing)
- 🐞 Fix ColorPicker HEX input allowing invalid characters. [#56752](https://github.com/ant-design/ant-design/pull/56752) [@treephesians](https://github.com/treephesians)
## 6.2.2
`2026-01-26`
- 🐞 Fix Button with href wrapped by Typography showing incorrect color and flickering outline on hover. [#56619](https://github.com/ant-design/ant-design/pull/56619) [@QdabuliuQ](https://github.com/QdabuliuQ)
- 🐞 Fix component token not taking effect for Button with `type="text"`. [#56291](https://github.com/ant-design/ant-design/pull/56291) [@QDyanbing](https://github.com/QDyanbing)
- 🐞 Fix where components within the Popover were affected by the state association with Form.Item. [#56728](https://github.com/ant-design/ant-design/pull/56728)
- 🐞 Fix the placeholders displayed incorrectly when selecting multiple items in Select. [#56675](https://github.com/ant-design/ant-design/pull/56675)
- 💄 Fix misaligned elements in Pagination when the global `fontSize` is increased. [#56715](https://github.com/ant-design/ant-design/pull/56715) [@QDyanbing](https://github.com/QDyanbing)
- 💄 Fix incorrect Drawer dragger position in RTL mode. [#56693](https://github.com/ant-design/ant-design/pull/56693) [@QdabuliuQ](https://github.com/QdabuliuQ)
## 6.2.1
`2026-01-20`
- 🐞 Fix Button child element's `className` be cleared if it contains two Chinese characters. [#56593](https://github.com/ant-design/ant-design/pull/56593) [@QdabuliuQ](https://github.com/QdabuliuQ)
- 🐞 Fix DatePicker DOM not updated bug after update `suffixIcon` as `null`. [#56637](https://github.com/ant-design/ant-design/pull/56637) [@AlanQtten](https://github.com/AlanQtten)
- 🐞 Fix Table content area border radius when set border radius for container. [#56478](https://github.com/ant-design/ant-design/pull/56478) [@QDyanbing](https://github.com/QDyanbing)
- 💄 Fix Card unexpected border radius for Body area. [#56653](https://github.com/ant-design/ant-design/pull/56653) [@ug-hero](https://github.com/ug-hero)
- 💄 MISC: Fix unexpected `undefined` and `null` in the injected styles. [#56636](https://github.com/ant-design/ant-design/pull/56636) [@li-jia-nan](https://github.com/li-jia-nan)
- 💄 MISC: Improve `background` transition to `background-color` for all components. [#56598](https://github.com/ant-design/ant-design/pull/56598) [@li-jia-nan](https://github.com/li-jia-nan)
- 🛠 Improve Grid use `genCssVar` method to generate more stable CSS variable names. [#56635](https://github.com/ant-design/ant-design/pull/56635) [@li-jia-nan](https://github.com/li-jia-nan)
- 🛠 Improve @ant-design/icons usage to avoid depend on full package since externals. [#56639](https://github.com/ant-design/ant-design/pull/56639) [@ShenHongFei](https://github.com/ShenHongFei)
## 6.2.0
`2026-01-13`
- 🛠 Button, Masonry, Mentions, Select, Space, Splitter, Steps and other components batch use `genCssVar` method to generate more stable CSS variable names. [#56562](https://github.com/ant-design/ant-design/pull/56562) [#56559](https://github.com/ant-design/ant-design/pull/56559) [#56557](https://github.com/ant-design/ant-design/pull/56557) [#56555](https://github.com/ant-design/ant-design/pull/56555) [#56550](https://github.com/ant-design/ant-design/pull/56550) [#56547](https://github.com/ant-design/ant-design/pull/56547) [#56546](https://github.com/ant-design/ant-design/pull/56546) [#56529](https://github.com/ant-design/ant-design/pull/56529) [@li-jia-nan](https://github.com/li-jia-nan)
- 🆕 QRCode adds `marginSize` property for displaying QR code margin area. [#56569](https://github.com/ant-design/ant-design/pull/56569) [@afc163](https://github.com/afc163)
- 🆕 Tour adds `keyboard` property to configure keyboard operations. [#56581](https://github.com/ant-design/ant-design/pull/56581) [@cactuser-Lu](https://github.com/cactuser-Lu)
- Tooltip
- 🆕 Tooltip adds `maxWidth` design token. [#56540](https://github.com/ant-design/ant-design/pull/56540) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Tooltip/Popover/Popconfirm can be closed with ESC by default. [#56492](https://github.com/ant-design/ant-design/pull/56492) [@aojunhao123](https://github.com/aojunhao123)
- 🛠 Steps remove useless styles. [#56565](https://github.com/ant-design/ant-design/pull/56565) [@li-jia-nan](https://github.com/li-jia-nan)
- 🆕 Form supports `tel` type validation. [#56533](https://github.com/ant-design/ant-design/pull/56533) [@guoyunhe](https://github.com/guoyunhe)
- 🐞 Fix Badge `ref` not working when using `text` property. [#56532](https://github.com/ant-design/ant-design/pull/56532) [@zombieJ](https://github.com/zombieJ)
- 🆕 Calendar and DatePicker `locale` configuration now supports partial content filling. [#56376](https://github.com/ant-design/ant-design/pull/56376) [@QDyanbing](https://github.com/QDyanbing)
- 🐞 Fix ConfigProvider `theme.cssVar` configuration not working for icons. [#56504](https://github.com/ant-design/ant-design/pull/56504) [@seanparmelee](https://github.com/seanparmelee)
- 🐞 Fix Collapse `items` semantic properties not working. [#56517](https://github.com/ant-design/ant-design/pull/56517) [@zombieJ](https://github.com/zombieJ)
- Modal
- 🆕 Modal supports `focusable.trap` to configure whether to lock focus within the Modal. [#56500](https://github.com/ant-design/ant-design/pull/56500) [@zombieJ](https://github.com/zombieJ)
- 🛠 Remove useless DOM structure from Modal and optimize focus capture to prevent accidental focus escape outside the Modal. [#56142](https://github.com/ant-design/ant-design/pull/56142) [@zombieJ](https://github.com/zombieJ)
- ConfigProvider
- 🆕 ConfigProvider supports `pagination` configuration for `totalBoundary` and `showSizeChanger` properties. [#56475](https://github.com/ant-design/ant-design/pull/56475) [@chiaweilee](https://github.com/chiaweilee)
- 🆕 ConfigProvider supports configuring Alert global icons. [#56241](https://github.com/ant-design/ant-design/pull/56241) [@guoyunhe](https://github.com/guoyunhe)
- Drawer
- 🆕 Drawer adds `focusable` to configure focus behavior after opening, supporting focus locking within the container and focus returning after closing. [#56463](https://github.com/ant-design/ant-design/pull/56463) [@zombieJ](https://github.com/zombieJ)
- 🐞 Fix Drawer `size` definition not supporting string type. [#56358](https://github.com/ant-design/ant-design/pull/56358) [@ug-hero](https://github.com/ug-hero)
- 🐞 Fix Image nested in Modal cannot be closed sequentially with Esc. [#56386](https://github.com/ant-design/ant-design/pull/56386) [@aojunhao123](https://github.com/aojunhao123)
- 🆕 Pagination supports `size` property. [#56009](https://github.com/ant-design/ant-design/pull/56009) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Breadcrumb supports `dropdownIcon` customization. [#56250](https://github.com/ant-design/ant-design/pull/56250) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Checkbox.Group supports `role` configuration. [#56126](https://github.com/ant-design/ant-design/pull/56126) [@Pareder](https://github.com/Pareder)
- 💄 Mentions fix invalid style `padding: undefined` in different sizes. [#56564](https://github.com/ant-design/ant-design/pull/56564) [@li-jia-nan](https://github.com/li-jia-nan)
- 🐞 Fix Select clear button alignment issue when `size="small"`. [#56525](https://github.com/ant-design/ant-design/pull/56525) [@QDyanbing](https://github.com/QDyanbing)
## 6.1.4
`2026-01-05`

View File

@@ -15,117 +15,6 @@ tag: vVERSION
---
## 6.3.0
`2026-02-10`
- ConfigProvider
- 🆕 ConfigProvider 支持 Modal 和 Drawer 的 `maskClosable` 全局配置。[#56739](https://github.com/ant-design/ant-design/pull/56739) [@luozz1994](https://github.com/luozz1994)
- 🆕 ConfigProvider 支持 DatePicker 和 TimePicker 的 `suffixIcon` 全局配置。[#56709](https://github.com/ant-design/ant-design/pull/56709) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 ConfigProvider 支持 Cascader 的 `expandIcon``loadingIcon` 全局配置。[#56482](https://github.com/ant-design/ant-design/pull/56482) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 ConfigProvider 支持 Table 的 `scroll` 全局配置。[#56628](https://github.com/ant-design/ant-design/pull/56628) [@Clayton](https://github.com/Clayton)
- 🆕 ConfigProvider 支持配置 App 的 `className``style`,以及 ColorPicker 的 `arrow` 属性。[#56573](https://github.com/ant-design/ant-design/pull/56573) [@zombieJ](https://github.com/zombieJ)
- 🆕 ConfigProvider 支持 Button 的 `loadingIcon` 全局配置。[#56439](https://github.com/ant-design/ant-design/pull/56439) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 ConfigProvider 支持 `rangePicker.separator` 全局配置。[#56499](https://github.com/ant-design/ant-design/pull/56499) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 ConfigProvider 支持 Form 的 `tooltipIcon``tooltipProps` 全局配置。[#56372](https://github.com/ant-design/ant-design/pull/56372) [@guoyunhe](https://github.com/guoyunhe)
- Upload
- 🆕 Upload 新增 `classNames.trigger``styles.trigger` 属性。[#56578](https://github.com/ant-design/ant-design/pull/56578) [@QdabuliuQ](https://github.com/QdabuliuQ)
- 🆕 Upload.Dragger 支持 `onDoubleClick` 事件。[#56579](https://github.com/ant-design/ant-design/pull/56579) [@ug-hero](https://github.com/ug-hero)
- 🐞 Upload 修复 `picture-card` / `picture-circle` 父节点缺少默认高度的问题。[#56864](https://github.com/ant-design/ant-design/pull/56864) [@wanpan11](https://github.com/wanpan11)
- 🆕 Grid 新增 `xxxl`1920px断点以适应 FHD 屏幕。[#56825](https://github.com/ant-design/ant-design/pull/56825) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Switch 语义化结构支持 `indicator` 定制。[#56710](https://github.com/ant-design/ant-design/pull/56710) [@zombieJ](https://github.com/zombieJ)
- Button
- 🐞 Button 修复暗色主题下 `color``hover``active` 状态颜色相反的问题。[#56872](https://github.com/ant-design/ant-design/pull/56872) [@zombieJ](https://github.com/zombieJ)
- 🐞 Button 修复边框尺寸未跟随 Design Token `lineWidth` 的问题。[#56683](https://github.com/ant-design/ant-design/pull/56683) [@zombieJ](https://github.com/zombieJ)
- Select
- 💄 Select 移除单选模式下额外的 `-content-value` div DOM优化语义化结构并支持通过 `classNames``styles` 覆盖。[#56811](https://github.com/ant-design/ant-design/pull/56811) [@zombieJ](https://github.com/zombieJ)
- 🐞 Select 修复 `notFoundContent` 不生效的问题。[#56756](https://github.com/ant-design/ant-design/pull/56756) [@QdabuliuQ](https://github.com/QdabuliuQ)
- Radio
- 🐞 Radio.Group 修复垂直排列时单选项出现多余右边距的问题。[#56909](https://github.com/ant-design/ant-design/pull/56909) [@jany55555](https://github.com/jany55555)
- 💄 Radio 移除 `icon` 子元素 `-inner` DOM 节点以更好适配语义化结构。[#56783](https://github.com/ant-design/ant-design/pull/56783) [@zombieJ](https://github.com/zombieJ)
- 💄 Modal & Drawer 默认关闭蒙层 blur 效果。[#56781](https://github.com/ant-design/ant-design/pull/56781) [@aojunhao123](https://github.com/aojunhao123)
- 🐞 Tooltip & Popover 修复弹出层动画起始位置偏左的问题。[#56887](https://github.com/ant-design/ant-design/pull/56887) [@zombieJ](https://github.com/zombieJ)
- 🐞 List 修复废弃组件配置的颜色相关 token 不生效的问题。[#56913](https://github.com/ant-design/ant-design/pull/56913) [@zombieJ](https://github.com/zombieJ)
- 🛠 Spin 重构 DOM 结构以对齐不同场景并支持全量语义化结构Semantic Structure。[#56852](https://github.com/ant-design/ant-design/pull/56852) [@zombieJ](https://github.com/zombieJ)
- ⌨️ ♿ Icon 为搜索图标 SVG 添加无障碍名称,改善屏幕阅读器支持。[#56521](https://github.com/ant-design/ant-design/pull/56521) [@huangkevin-apr](https://github.com/huangkevin-apr)
- 🐞 Cascader 修复搜索模式下选择选项并关闭时,过滤列表立即还原影响体验的问题。[#56764](https://github.com/ant-design/ant-design/pull/56764) [@zombieJ](https://github.com/zombieJ)
- ⌨️ ♿ Tree 优化无障碍支持。[#56716](https://github.com/ant-design/ant-design/pull/56716) [@aojunhao123](https://github.com/aojunhao123)
- 🐞 ColorPicker 选择块支持语义化结构,并修复 `root` 语义化错误应用到弹出元素的问题。[#56607](https://github.com/ant-design/ant-design/pull/56607) [@zombieJ](https://github.com/zombieJ)
- 💄 Avatar 将 `size` 默认值从 `default` 改为 `medium` 以保持一致性。[#56440](https://github.com/ant-design/ant-design/pull/56440) [@guoyunhe](https://github.com/guoyunhe)
- 💄 Checkbox 移除 `icon` 子元素 `-inner` DOM 节点以更好适配语义化结构。[#56783](https://github.com/ant-design/ant-design/pull/56783) [@zombieJ](https://github.com/zombieJ)
- MISC
- 🐞 MISC: 修复 UMD 版本中 React Compiler 兼容性问题,现已默认关闭。[#56830](https://github.com/ant-design/ant-design/pull/56830) [@zombieJ](https://github.com/zombieJ)
- 🛠 精简 `styles``classNames` 类型定义,使其更规范。[#56758](https://github.com/ant-design/ant-design/pull/56758) [@crazyair](https://github.com/crazyair)
## 6.2.3
`2026-02-02`
- Button
- 🐞 修复 Button `defaultBg``defaultColor``defaultHoverColor``defaultActiveColor` token 不生效的问题。[#56238](https://github.com/ant-design/ant-design/pull/56238) [@ug-hero](https://github.com/ug-hero)
- 🐞 修复 Button 默认 token 不生效的问题。[#56719](https://github.com/ant-design/ant-design/pull/56719) [@unknowntocka](https://github.com/unknowntocka)
- 🐞 修复 Button `variant="solid"` 在 Space.Compact 中边框显示异常的问题。[#56486](https://github.com/ant-design/ant-design/pull/56486) [@Pareder](https://github.com/Pareder)
- 🐞 修复 Input.TextArea ref 缺少 `nativeElement` 属性的问题。[#56803](https://github.com/ant-design/ant-design/pull/56803) [@smith3816](https://github.com/smith3816)
- 🐞 修复 Flex 使用 `orientation` 时默认 `align` 不生效的问题。[#55950](https://github.com/ant-design/ant-design/pull/55950) [@YingtaoMo](https://github.com/YingtaoMo)
- 🐞 修复 Typography 链接选择器特异性过低导致样式被覆盖的问题。[#56759](https://github.com/ant-design/ant-design/pull/56759) [@QDyanbing](https://github.com/QDyanbing)
- 🐞 修复 ColorPicker HEX 输入框可以输入无效字符的问题。[#56752](https://github.com/ant-design/ant-design/pull/56752) [@treephesians](https://github.com/treephesians)
## 6.2.2
`2026-01-26`
- 🐞 修复被 Typography 包裹的带 href 的 Button 显示错误颜色和 hover 时 outline 闪烁的问题。[#56619](https://github.com/ant-design/ant-design/pull/56619) [@QdabuliuQ](https://github.com/QdabuliuQ)
- 🐞 修复 Button `type="text"` 时组件 Token 不生效的问题。[#56291](https://github.com/ant-design/ant-design/pull/56291) [@QDyanbing](https://github.com/QDyanbing)
- 🐞 修复 Popover 内组件被 Form.Item 状态关联影响的问题。[#56728](https://github.com/ant-design/ant-design/pull/56728)
- 🐞 修复 Select 多选时占位符显示异常的问题。[#56675](https://github.com/ant-design/ant-design/pull/56675)
- 💄 修复 Pagination 全局 `fontSize` 变大时各元素上下错位的问题。[#56715](https://github.com/ant-design/ant-design/pull/56715) [@QDyanbing](https://github.com/QDyanbing)
- 💄 修复 Drawer 在 rtl 模式下 dragger 位置错误的样式问题。[#56693](https://github.com/ant-design/ant-design/pull/56693) [@QdabuliuQ](https://github.com/QdabuliuQ)
## 6.2.1
`2026-01-20`
- 🐞 修复 Button 子元素为包含两个中文字符的标签时,原有 `className` 被清空的问题。[#56593](https://github.com/ant-design/ant-design/pull/56593) [@QdabuliuQ](https://github.com/QdabuliuQ)
- 🐞 修复 DatePicker 在设置 `suffixIcon``null` 后不会更新 DOM 的问题。[#56637](https://github.com/ant-design/ant-design/pull/56637) [@AlanQtten](https://github.com/AlanQtten)
- 🐞 修复 Table 容器设置圆角时,内部内容区域圆角不一致的问题。[#56478](https://github.com/ant-design/ant-design/pull/56478) [@QDyanbing](https://github.com/QDyanbing)
- 💄 修复 Card Body 区域有非预期圆角值的问题。[#56653](https://github.com/ant-design/ant-design/pull/56653) [@ug-hero](https://github.com/ug-hero)
- 💄 杂项:修复 `undefined``null` 值被注入到 CSS 的问题。[#56636](https://github.com/ant-design/ant-design/pull/56636) [@li-jia-nan](https://github.com/li-jia-nan)
- 💄 杂项:优化所有组件中的 `background` 过渡为 `background-color`。[#56598](https://github.com/ant-design/ant-design/pull/56598) [@li-jia-nan](https://github.com/li-jia-nan)
- 🛠 优化 Grid 使用 `genCssVar` 方法以生成更加稳定的 CSS 变量名。[#56635](https://github.com/ant-design/ant-design/pull/56635) [@li-jia-nan](https://github.com/li-jia-nan)
- 🛠 优化 @ant-design/icons 引入方式为独立图标引入,避免被 externals 增加前置依赖。[#56639](https://github.com/ant-design/ant-design/pull/56639) [@ShenHongFei](https://github.com/ShenHongFei)
## 6.2.0
`2026-01-13`
- 🛠 Button、Masonry、Mentions、Select、Space、Splitter、Steps 等组件批量使用 `genCssVar` 方法以生成更加稳定的 css 变量名。[#56562](https://github.com/ant-design/ant-design/pull/56562) [#56559](https://github.com/ant-design/ant-design/pull/56559) [#56557](https://github.com/ant-design/ant-design/pull/56557) [#56555](https://github.com/ant-design/ant-design/pull/56555) [#56550](https://github.com/ant-design/ant-design/pull/56550) [#56547](https://github.com/ant-design/ant-design/pull/56547) [#56546](https://github.com/ant-design/ant-design/pull/56546) [#56529](https://github.com/ant-design/ant-design/pull/56529) [@li-jia-nan](https://github.com/li-jia-nan)
- 🆕 QRCode 新增 `marginSize` 属性用于展示二维码留白区。[#56569](https://github.com/ant-design/ant-design/pull/56569) [@afc163](https://github.com/afc163)
- 🆕 Tour 新增 `keyboard` 属性以配置键盘操作。[#56581](https://github.com/ant-design/ant-design/pull/56581) [@cactuser-Lu](https://github.com/cactuser-Lu)
- Tooltip
- 🆕 Tooltip 增加 `maxWidth` design token。[#56540](https://github.com/ant-design/ant-design/pull/56540) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Tooltip/Popover/Popconfirm 默认情况下可以通过 ESC 关闭。[#56492](https://github.com/ant-design/ant-design/pull/56492) [@aojunhao123](https://github.com/aojunhao123)
- 🛠 Steps 移除无用的样式。[#56565](https://github.com/ant-design/ant-design/pull/56565) [@li-jia-nan](https://github.com/li-jia-nan)
- 🆕 Form 支持 `tel` 类型校验。[#56533](https://github.com/ant-design/ant-design/pull/56533) [@guoyunhe](https://github.com/guoyunhe)
- 🐞 修复 Badge 在使用 `text` 属性时,`ref` 无效的问题。[#56532](https://github.com/ant-design/ant-design/pull/56532) [@zombieJ](https://github.com/zombieJ)
- 🆕 Calendar 和 DatePicker 的 `locale` 配置现在支持只填充部分内容。[#56376](https://github.com/ant-design/ant-design/pull/56376) [@QDyanbing](https://github.com/QDyanbing)
- 🐞 修复 ConfigProvider 配置 `theme.cssVar` 对图标无效的问题。[#56504](https://github.com/ant-design/ant-design/pull/56504) [@seanparmelee](https://github.com/seanparmelee)
- 🐞 修复 Collapse `items` 语义化属性无效的问题。[#56517](https://github.com/ant-design/ant-design/pull/56517) [@zombieJ](https://github.com/zombieJ)
- Modal
- 🆕 Modal 支持 `focusable.trap` 以配置是否将焦点锁定在 Modal 内部。[#56500](https://github.com/ant-design/ant-design/pull/56500) [@zombieJ](https://github.com/zombieJ)
- 🛠 移除 Modal 无用的 DOM 结构并且优化焦点捕获以防止意外的焦点逃逸到 Modal 外的情况。[#56142](https://github.com/ant-design/ant-design/pull/56142) [@zombieJ](https://github.com/zombieJ)
- ConfigProvider
- 🆕 ConfigProvider 支持 `pagination` 配置 `totalBoundary``showSizeChanger` 属性。[#56475](https://github.com/ant-design/ant-design/pull/56475) [@chiaweilee](https://github.com/chiaweilee)
- 🆕 ConfigProvider 支持配置 Alert 全局图标。[#56241](https://github.com/ant-design/ant-design/pull/56241) [@guoyunhe](https://github.com/guoyunhe)
- Drawer
- 🆕 Drawer 新增 `focusable` 以配置展开后的焦点行为,支持配置锁定焦点在框内、关闭后是否返回焦点。[#56463](https://github.com/ant-design/ant-design/pull/56463) [@zombieJ](https://github.com/zombieJ)
- 🐞 修复 Drawer `size` 定义不支持 string 的问题。[#56358](https://github.com/ant-design/ant-design/pull/56358) [@ug-hero](https://github.com/ug-hero)
- 🐞 修复 Image 嵌套在 Modal 内时Esc无法顺序关闭。[#56386](https://github.com/ant-design/ant-design/pull/56386) [@aojunhao123](https://github.com/aojunhao123)
- 🆕 Pagination 支持 `size` 属性。[#56009](https://github.com/ant-design/ant-design/pull/56009) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Breadcrumb 支持 `dropdownIcon` 自定义。[#56250](https://github.com/ant-design/ant-design/pull/56250) [@guoyunhe](https://github.com/guoyunhe)
- 🆕 Checkbox.Group 支持 `role` 配置。[#56126](https://github.com/ant-design/ant-design/pull/56126) [@Pareder](https://github.com/Pareder)
- 💄 Mentions 修复不同尺寸下 `padding: undefined` 的无效样式。[#56564](https://github.com/ant-design/ant-design/pull/56564) [@li-jia-nan](https://github.com/li-jia-nan)
- 🐞 修复 Select 在 `size="small"` 时,清除按钮位置对齐问题。[#56525](https://github.com/ant-design/ant-design/pull/56525) [@QDyanbing](https://github.com/QDyanbing)
## 6.1.4
`2026-01-05`

View File

@@ -14,7 +14,7 @@
## ❤️ 赞助者 [![](https://opencollective.com/ant-design/tiers/sponsors/badge.svg?label=Sponsors&color=brightgreen)](https://opencollective.com/ant-design/contribute/sponsors-218)
[![TRACTIAN](https://images.opencollective.com/tractian/0235da9/logo/256.png?height=80)](https://tractian.com) [![LobeHub](https://images.opencollective.com/lobehub/ac07c4c/logo/256.png?height=80)](https://lobehub.com/)
[![](https://opencollective.com/ant-design/tiers/sponsors.svg?avatarHeight=72)](https://opencollective.com/ant-design/contribute/sponsors-218/checkout)
[npm-image]: https://img.shields.io/npm/v/antd.svg?style=flat-square
[npm-url]: https://npmjs.org/package/antd
@@ -115,7 +115,6 @@ export default App;
- [首页](https://ant.design/)
- [所有组件](https://ant.design/components/overview-cn)
- [赞助](https://ant.design/docs/react/sponsor)
- [更新日志](CHANGELOG.zh-CN.md)
- [React 底层基础组件](https://react-component.github.io/)
- [🆕 Ant Design X](https://x.ant.design/index-cn)
@@ -187,7 +186,7 @@ $ npm start
</table>
<a href="https://openomy.app/github/ant-design/ant-design" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=ant-design/ant-design&chart=bubble&latestMonth=3" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
<img src="https://openomy.app/svg?repo=ant-design/ant-design&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
请参考[贡献指南](https://ant.design/docs/react/contributing-cn).

View File

@@ -14,7 +14,7 @@ An enterprise-class UI design language and React UI library.
## ❤️ Sponsors [![](https://opencollective.com/ant-design/tiers/sponsors/badge.svg?label=Sponsors&color=brightgreen)](https://opencollective.com/ant-design/contribute/sponsors-218)
[![TRACTIAN](https://images.opencollective.com/tractian/0235da9/logo/256.png?height=80)](https://tractian.com) [![LobeHub](https://images.opencollective.com/lobehub/ac07c4c/logo/256.png?height=80)](https://lobehub.com/)
[![](https://opencollective.com/ant-design/tiers/sponsors.svg?avatarHeight=72)](https://opencollective.com/ant-design/contribute/sponsors-218/checkout)
[npm-image]: https://img.shields.io/npm/v/antd.svg?style=flat-square
[npm-url]: https://npmjs.org/package/antd
@@ -98,7 +98,6 @@ export default () => (
- [Home page](https://ant.design/)
- [Components Overview](https://ant.design/components/overview)
- [Sponsor](https://ant.design/docs/react/sponsor)
- [Change Log](CHANGELOG.en-US.md)
- [rc-components](https://react-component.github.io/)
- [🆕 Ant Design X](https://x.ant.design/index-cn)

View File

@@ -2,7 +2,7 @@
## Supported Versions
Versions of ant-design that are currently supported with security updates:
Use this section to tell people about which versions of your project are currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
@@ -12,14 +12,6 @@ Versions of ant-design that are currently supported with security updates:
## Reporting a Vulnerability
Security vulnerabilities in ant-design are handled by the ant-design team.
Use this section to tell people how to report a vulnerability.
### How to Report
If you find a security vulnerability, please **do not** open a public issue. Instead, please send an email to **security@ant.design**. Include details about:
- The affected version(s)
- Steps to reproduce the vulnerability
- Potential impact of the vulnerability
Our team will review your report and respond as soon as possible. We appreciate your help in reporting security issues responsibly.
Tell them where to go, how often they can expect to get an update on a reported vulnerability, what to expect if the vulnerability is accepted or declined, etc.

View File

@@ -1,24 +1,35 @@
import fs from 'node:fs';
import path from 'node:path';
const fs = require('fs');
const blogDir = path.join(__dirname, '../../docs/blog');
const path = require('path');
const blogList = fs
.readdirSync(blogDir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.filter((file) => file.endsWith('.en-US.md'))
.map((file) => path.join(blogDir, file));
const blogList = [
'check-conduct',
'contributor-development-maintenance-guide',
'css-in-js',
'extract-ssr',
'getContainer',
'github-actions-workflow',
'issue-helper',
'mock-project-build',
'modal-hook-order',
'testing-migrate',
'render-times',
'to-be-collaborator',
'tooltip-align',
'tree-shaking',
'why-not-static',
].map((blogName) => path.join(__dirname, `../../docs/blog/${blogName}.en-US.md`));
describe('Chinese detected in en-US blog', () => {
describe('blog', () => {
it('should not include Chinese in en-US blog', () => {
for (const blog of blogList) {
const data = fs.readFileSync(blog, 'utf-8');
const includeChinese = /[\u4E00-\u9FA5]/.test(data);
if (includeChinese) {
console.error('❌ 检测到中文:', blog);
}
expect(includeChinese).toBe(false);
}
blogList.forEach((blog) => {
fs.readFile(blog, (err: NodeJS.ErrnoException | null, data: Buffer) => {
if (err) {
return;
}
const includeChinese = /[\u4E00-\u9FA5]/.test(data.toString());
expect(includeChinese).toBe(false);
});
});
});
});

View File

@@ -26,7 +26,7 @@ import {
import type { ZIndexConsumer, ZIndexContainer } from '../hooks/useZIndex';
import { consumerBaseZIndexOffset, containerBaseZIndexOffset, useZIndex } from '../hooks/useZIndex';
import { resetWarned } from '../warning';
import ZIndexContext from '../zindexContext';
import zIndexContext from '../zindexContext';
// TODO: Remove this. Mock for React 19
jest.mock('react-dom', () => {
@@ -45,7 +45,7 @@ const WrapWithProvider: React.FC<PropsWithChildren<{ container: ZIndexContainer
container,
}) => {
const [, contextZIndex] = useZIndex(container);
return <ZIndexContext.Provider value={contextZIndex}>{children}</ZIndexContext.Provider>;
return <zIndexContext.Provider value={contextZIndex}>{children}</zIndexContext.Provider>;
};
const containerComponent: Partial<
@@ -130,10 +130,10 @@ const items: MenuProps['items'] = [
];
const consumerComponent: Partial<
Record<ZIndexConsumer, React.FC<Readonly<{ rootClassName: string; ref?: React.Ref<any> }>>>
Record<ZIndexConsumer, React.FC<Readonly<{ rootClassName: string }>>>
> = {
SelectLike: ({ rootClassName, ref, ...props }) => (
<div ref={ref}>
SelectLike: ({ rootClassName, ...props }) => (
<>
<Select
{...props}
rootClassName={`${rootClassName} comp-item comp-Select`}
@@ -159,7 +159,7 @@ const consumerComponent: Partial<
open
/>
<ColorPicker {...props} open rootClassName={`${rootClassName} comp-item comp-ColorPicker`} />
</div>
</>
),
Dropdown: (props) => (
<Dropdown
@@ -175,15 +175,15 @@ const consumerComponent: Partial<
<button type="button">test</button>
</Dropdown>
),
DatePicker: ({ rootClassName, ref, ...props }) => (
<div ref={ref}>
DatePicker: ({ rootClassName, ...props }) => (
<>
<DatePicker {...props} rootClassName={`${rootClassName} comp-item comp-DatePicker`} open />
<DatePicker.TimePicker
{...props}
rootClassName={`${rootClassName} comp-item comp-TimePicker`}
open
/>
</div>
</>
),
Menu: (props) => <Menu {...props} items={items} defaultOpenKeys={['SubMenu']} />,
ImagePreview: ({ rootClassName }: ImageProps) => (

View File

@@ -4,15 +4,11 @@ import { clsx } from 'clsx';
import mountTest from '../../../tests/shared/mountTest';
import { act, fireEvent, getByText, render, waitFakeTimer } from '../../../tests/utils';
import Checkbox from '../../checkbox';
import { defaultPrefixCls } from '../../config-provider';
import { genCssVar } from '../../theme/util/genStyleUtils';
import Wave from '../wave';
import { TARGET_CLS } from '../wave/interface';
(global as any).isVisible = true;
const [varName] = genCssVar(defaultPrefixCls, 'wave');
// TODO: Remove this. Mock for React 19
jest.mock('react-dom', () => {
const realReactDOM = jest.requireActual('react-dom');
@@ -143,8 +139,9 @@ describe('Wave component', () => {
fireEvent.click(container.querySelector('button')!);
waitRaf();
const style = getWaveStyle();
expect(style[varName('color')]).toBe(undefined);
expect(style['--wave-color']).toEqual(undefined);
unmount();
});
@@ -162,7 +159,7 @@ describe('Wave component', () => {
waitRaf();
const style = getWaveStyle();
expect(style[varName('color')]).toBe('rgb(255, 0, 0)');
expect(style['--wave-color']).toEqual('rgb(255, 0, 0)');
unmount();
});
@@ -178,7 +175,7 @@ describe('Wave component', () => {
waitRaf();
const style = getWaveStyle();
expect(style[varName('color')]).toBe('rgb(0, 0, 255)');
expect(style['--wave-color']).toEqual('rgb(0, 0, 255)');
unmount();
});
@@ -194,7 +191,7 @@ describe('Wave component', () => {
waitRaf();
const style = getWaveStyle();
expect(style[varName('color')]).toBe('rgb(0, 128, 0)');
expect(style['--wave-color']).toEqual('rgb(0, 128, 0)');
unmount();
});
@@ -210,7 +207,7 @@ describe('Wave component', () => {
waitRaf();
const style = getWaveStyle();
expect(style[varName('color')]).toBe('rgb(255, 255, 0)');
expect(style['--wave-color']).toEqual('rgb(255, 255, 0)');
unmount();
});
@@ -291,7 +288,7 @@ describe('Wave component', () => {
waitRaf();
const style = getWaveStyle();
expect(style[varName('color')]).toBe('rgb(255, 0, 0)');
expect(style['--wave-color']).toEqual('rgb(255, 0, 0)');
unmount();
});
@@ -309,7 +306,7 @@ describe('Wave component', () => {
waitRaf();
const style = getWaveStyle();
expect(style[varName('color')]).toBe('red');
expect(style['--wave-color']).toEqual('red');
unmount();
});
@@ -377,7 +374,7 @@ describe('Wave component', () => {
expect(document.querySelector('.ant-wave')).toBeTruthy();
const style = getWaveStyle();
expect(style[varName('color')]).toBe('rgb(255, 0, 0)');
expect(style['--wave-color']).toEqual('rgb(255, 0, 0)');
unmount();
});

View File

@@ -4,16 +4,16 @@ import { isValidElement } from 'react';
import type { TooltipProps } from '../tooltip';
import isNonNullable from './isNonNullable';
const convertToTooltipProps = <P extends TooltipProps>(tooltip: P | ReactNode, context?: P) => {
const convertToTooltipProps = <P extends TooltipProps>(tooltip: P | ReactNode) => {
if (!isNonNullable(tooltip)) {
return null;
}
if (typeof tooltip === 'object' && !isValidElement(tooltip)) {
return { ...context, ...tooltip } as P;
return tooltip as P;
}
return { ...context, title: tooltip } as P;
return { title: tooltip } as P;
};
export default convertToTooltipProps;

View File

@@ -7,21 +7,23 @@ export type SemanticSchema = { _default?: string } & {
[key: `${ValidChar}${string}`]: SemanticSchema;
};
export type SemanticClassNames<Name extends string> = Partial<Record<Name, string>>;
export type SemanticStyles<Name extends string> = Partial<Record<Name, React.CSSProperties>>;
export type Resolvable<T, P extends AnyObject> = T | ((info: { props: P }) => T);
export type SemanticClassNamesType<
Props extends AnyObject,
SemanticClassNames extends Record<PropertyKey, string>,
SemanticName extends string,
NestedStructure extends EmptyObject = EmptyObject,
> = Resolvable<Readonly<SemanticClassNames>, Props> & NestedStructure;
> = Resolvable<Readonly<SemanticClassNames<SemanticName>>, Props> & NestedStructure;
export type SemanticStylesType<
Props extends AnyObject,
SemanticStyles extends Record<PropertyKey, React.CSSProperties>,
SemanticName extends string,
NestedStructure extends EmptyObject = EmptyObject,
> = Resolvable<Readonly<SemanticStyles>, Props> & NestedStructure;
export type SemanticType<P = any, T = any> = T | ((info: { props: P }) => T);
> = Resolvable<Readonly<SemanticStyles<SemanticName>>, Props> & NestedStructure;
// ========================= ClassNames =========================
export const mergeClassNames = <

View File

@@ -3,52 +3,35 @@ import { useMemo } from 'react';
export interface MaskConfig {
enabled?: boolean;
blur?: boolean;
closable?: boolean;
}
export type MaskType = MaskConfig | boolean;
export const normalizeMaskConfig = (mask?: MaskType, maskClosable?: boolean): MaskConfig => {
let maskConfig: MaskConfig = {};
const normalizeMaskConfig = (mask?: MaskType): MaskConfig => {
if (mask && typeof mask === 'object') {
maskConfig = mask;
return mask;
}
if (typeof mask === 'boolean') {
maskConfig = {
return {
enabled: mask,
blur: mask,
};
}
if (maskConfig.closable === undefined && maskClosable !== undefined) {
maskConfig.closable = maskClosable;
}
return maskConfig;
return {};
};
export const useMergedMask = (
mask?: MaskType,
contextMask?: MaskType,
prefixCls?: string,
maskClosable?: boolean,
): [
config: boolean,
maskBlurClassName: { [key: string]: string | undefined },
maskClosable: boolean,
] => {
): [boolean, { [key: string]: string | undefined }] => {
return useMemo(() => {
const maskConfig = normalizeMaskConfig(mask, maskClosable);
const maskConfig = normalizeMaskConfig(mask);
const contextMaskConfig = normalizeMaskConfig(contextMask);
const mergedConfig: MaskConfig = {
blur: false,
...contextMaskConfig,
...maskConfig,
closable: maskConfig.closable ?? maskClosable ?? contextMaskConfig.closable ?? true,
};
const mergedConfig: MaskConfig = { ...contextMaskConfig, ...maskConfig };
const className = mergedConfig.blur ? `${prefixCls}-mask-blur` : undefined;
const className = mergedConfig.blur !== false ? `${prefixCls}-mask-blur` : undefined;
return [mergedConfig.enabled !== false, { mask: className }, !!mergedConfig.closable];
}, [mask, contextMask, prefixCls, maskClosable]);
return [mergedConfig.enabled !== false, { mask: className }];
}, [mask, contextMask, prefixCls]);
};

View File

@@ -2,7 +2,7 @@ import React from 'react';
import useToken from '../../theme/useToken';
import { devUseWarning } from '../warning';
import ZIndexContext from '../zindexContext';
import zIndexContext from '../zindexContext';
export type ZIndexContainer =
| 'Modal'
@@ -61,7 +61,7 @@ export const useZIndex = (
customZIndex?: number,
): ReturnResult => {
const [, token] = useToken();
const parentZIndex = React.useContext(ZIndexContext);
const parentZIndex = React.useContext(zIndexContext);
const isContainer = isContainerType(componentType);
let result: ReturnResult;

View File

@@ -3,14 +3,12 @@ import React from 'react';
import type { GlobalToken } from '../theme/internal';
import { useToken } from '../theme/internal';
export const responsiveArray = ['xxxl', 'xxl', 'xl', 'lg', 'md', 'sm', 'xs'] as const;
export const responsiveArrayReversed = [...responsiveArray].reverse();
export type Breakpoint = (typeof responsiveArray)[number];
export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type BreakpointMap = Record<Breakpoint, string>;
export type ScreenMap = Partial<Record<Breakpoint, boolean>>;
export type ScreenSizeMap = Partial<Record<Breakpoint, number>>;
export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'];
type SubscribeFunc = (screens: ScreenMap) => void;
const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({
@@ -20,7 +18,6 @@ const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({
lg: `(min-width: ${token.screenLG}px)`,
xl: `(min-width: ${token.screenXL}px)`,
xxl: `(min-width: ${token.screenXXL}px)`,
xxxl: `(min-width: ${token.screenXXXL}px)`,
});
/**

View File

@@ -6,8 +6,6 @@ import { composeRef } from '@rc-component/util/lib/ref';
import { clsx } from 'clsx';
import type { WaveProps } from '.';
import { ConfigContext } from '../../config-provider';
import { genCssVar } from '../../theme/util/genStyleUtils';
import { TARGET_CLS } from './interface';
import type { ShowWaveEffect } from './interface';
import { getTargetWaveColor } from './util';
@@ -23,18 +21,12 @@ export interface WaveEffectProps {
colorSource?: WaveProps['colorSource'];
}
const WaveEffect: React.FC<WaveEffectProps> = (props) => {
const WaveEffect = (props: WaveEffectProps) => {
const { className, target, component, colorSource } = props;
const divRef = React.useRef<HTMLDivElement>(null);
const { getPrefixCls } = React.useContext(ConfigContext);
const rootPrefixCls = getPrefixCls();
const [varName] = genCssVar(rootPrefixCls, 'wave');
// ===================== Effect =====================
const [waveColor, setWaveColor] = React.useState<string | null>(null);
const [color, setWaveColor] = React.useState<string | null>(null);
const [borderRadius, setBorderRadius] = React.useState<number[]>([]);
const [left, setLeft] = React.useState(0);
const [top, setTop] = React.useState(0);
@@ -50,8 +42,8 @@ const WaveEffect: React.FC<WaveEffectProps> = (props) => {
borderRadius: borderRadius.map((radius) => `${radius}px`).join(' '),
};
if (waveColor) {
waveStyle[varName('color')] = waveColor;
if (color) {
waveStyle['--wave-color'] = color;
}
function syncPos() {

View File

@@ -1,48 +1,40 @@
import type { CSSObject } from '@ant-design/cssinjs';
import { genComponentStyleHook } from '../../theme/internal';
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genCssVar } from '../../theme/util/genStyleUtils';
// biome-ignore lint/suspicious/noEmptyInterface: ComponentToken need to be empty by default
export interface ComponentToken {}
export interface WaveToken extends FullToken<'Wave'> {}
const genWaveStyle: GenerateStyle<WaveToken, CSSObject> = (token) => {
const {
componentCls,
colorPrimary,
motionDurationSlow,
motionEaseInOut,
motionEaseOutCirc,
antCls,
} = token;
const [, varRef] = genCssVar(antCls, 'wave');
const genWaveStyle: GenerateStyle<WaveToken> = (token) => {
const { componentCls, colorPrimary } = token;
return {
[componentCls]: {
position: 'absolute',
background: 'transparent',
pointerEvents: 'none',
boxSizing: 'border-box',
color: varRef('color', colorPrimary),
color: `var(--wave-color, ${colorPrimary})`,
boxShadow: `0 0 0 0 currentcolor`,
opacity: 0.2,
// =================== Motion ===================
'&.wave-motion-appear': {
transition: [`box-shadow 0.4s`, `opacity 2s`]
.map((prop) => `${prop} ${motionEaseOutCirc}`)
.join(','),
transition: [
`box-shadow 0.4s ${token.motionEaseOutCirc}`,
`opacity 2s ${token.motionEaseOutCirc}`,
].join(','),
'&-active': {
boxShadow: `0 0 0 6px currentcolor`,
opacity: 0,
},
'&.wave-quick': {
transition: [`box-shadow`, `opacity`]
.map((prop) => `${prop} ${motionDurationSlow} ${motionEaseInOut}`)
.join(','),
transition: [
`box-shadow ${token.motionDurationSlow} ${token.motionEaseInOut}`,
`opacity ${token.motionDurationSlow} ${token.motionEaseInOut}`,
].join(','),
},
},
},

View File

@@ -1,9 +1,9 @@
import React from 'react';
const ZIndexContext = React.createContext<number | undefined>(undefined);
const zIndexContext = React.createContext<number | undefined>(undefined);
if (process.env.NODE_ENV !== 'production') {
ZIndexContext.displayName = 'ZIndexContext';
zIndexContext.displayName = 'zIndexContext';
}
export default ZIndexContext;
export default zIndexContext;

Some files were not shown because too many files have changed in this diff Show More