chore: auto merge branches (#38619)

chore: master merge next
This commit is contained in:
github-actions[bot] 2022-11-18 05:36:31 +00:00 committed by GitHub
commit aedda24ad3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2700 changed files with 421665 additions and 424498 deletions

View File

@ -1,195 +1,32 @@
const fs = require('fs');
const path = require('path');
const defaultVars = require('./scripts/default-vars');
const darkVars = require('./scripts/dark-vars');
const compactVars = require('./scripts/compact-vars');
function generateThemeFileContent(theme) {
return `const { ${theme}ThemeSingle } = require('./theme');\nconst defaultTheme = require('./default-theme');\n
module.exports = {
...defaultTheme,
...${theme}ThemeSingle
}`;
}
// We need compile additional content for antd user
function finalizeCompile() {
if (fs.existsSync(path.join(__dirname, './lib'))) {
// Build a entry less file to dist/antd.less
const componentsPath = path.join(process.cwd(), 'components');
let componentsLessContent = '';
// Build components in one file: lib/style/components.less
fs.readdir(componentsPath, (err, files) => {
files.forEach(file => {
if (fs.existsSync(path.join(componentsPath, file, 'style', 'index.less'))) {
componentsLessContent += `@import "../${path.posix.join(
file,
'style',
'index-pure.less',
)}";\n`;
}
});
fs.writeFileSync(
path.join(process.cwd(), 'lib', 'style', 'components.less'),
componentsLessContent,
);
});
}
}
function buildThemeFile(theme, vars) {
// Build less entry file: dist/antd.${theme}.less
if (theme !== 'default') {
fs.writeFileSync(
path.join(process.cwd(), 'dist', `antd.${theme}.less`),
`@import "../lib/style/${theme}.less";\n@import "../lib/style/components.less";`,
if (fs.existsSync(path.join(__dirname, './es'))) {
// Build less entry file: dist/antd.less
fs.copyFileSync(
path.join(process.cwd(), 'components', 'style', 'reset.css'),
path.join(process.cwd(), 'es', 'style', 'reset.css'),
);
// eslint-disable-next-line no-console
console.log(`Built a entry less file to dist/antd.${theme}.less`);
} else {
fs.writeFileSync(
path.join(process.cwd(), 'dist', `default-theme.js`),
`module.exports = ${JSON.stringify(vars, null, 2)};\n`,
);
return;
}
// Build ${theme}.js: dist/${theme}-theme.js, for less-loader
fs.writeFileSync(
path.join(process.cwd(), 'dist', `theme.js`),
`const ${theme}ThemeSingle = ${JSON.stringify(vars, null, 2)};\n`,
{
flag: 'a',
},
);
fs.writeFileSync(
path.join(process.cwd(), 'dist', `${theme}-theme.js`),
generateThemeFileContent(theme),
);
// eslint-disable-next-line no-console
console.log(`Built a ${theme} theme js file to dist/${theme}-theme.js`);
}
function finalizeDist() {
if (fs.existsSync(path.join(__dirname, './dist'))) {
// Build less entry file: dist/antd.less
fs.writeFileSync(
path.join(process.cwd(), 'dist', 'antd.less'),
'@import "../lib/style/default.less";\n@import "../lib/style/components.less";',
);
// eslint-disable-next-line no-console
fs.writeFileSync(
path.join(process.cwd(), 'dist', 'theme.js'),
`const defaultTheme = require('./default-theme.js');\n`,
);
// eslint-disable-next-line no-console
console.log('Built a entry less file to dist/antd.less');
buildThemeFile('default', defaultVars);
buildThemeFile('dark', darkVars);
buildThemeFile('compact', compactVars);
buildThemeFile('variable', {});
fs.writeFileSync(
path.join(process.cwd(), 'dist', `theme.js`),
`
function getThemeVariables(options = {}) {
let themeVar = {
'hack': \`true;@import "\${require.resolve('antd/lib/style/color/colorPalette.less')}";\`,
...defaultTheme
};
if(options.dark) {
themeVar = {
...themeVar,
...darkThemeSingle
}
}
if(options.compact){
themeVar = {
...themeVar,
...compactThemeSingle
}
}
return themeVar;
}
module.exports = {
darkThemeSingle,
compactThemeSingle,
getThemeVariables
}`,
{
flag: 'a',
},
fs.copyFileSync(
path.join(process.cwd(), 'components', 'style', 'reset.css'),
path.join(process.cwd(), 'dist', 'reset.css'),
);
}
}
function isComponentStyleEntry(file) {
return file.path.match(/style(\/|\\)index\.tsx/);
}
function needTransformStyle(content) {
return content.includes('../../style/index.less') || content.includes('./index.less');
}
module.exports = {
compile: {
includeLessFile: [/(\/|\\)components(\/|\\)style(\/|\\)default.less$/],
transformTSFile(file) {
if (isComponentStyleEntry(file)) {
let content = file.contents.toString();
if (needTransformStyle(content)) {
const cloneFile = file.clone();
// Origin
content = content.replace('../../style/index.less', '../../style/default.less');
cloneFile.contents = Buffer.from(content);
return cloneFile;
}
}
},
transformFile(file) {
if (isComponentStyleEntry(file)) {
const indexLessFilePath = file.path.replace('index.tsx', 'index.less');
if (fs.existsSync(indexLessFilePath)) {
// We put origin `index.less` file to `index-pure.less`
const pureFile = file.clone();
pureFile.contents = Buffer.from(fs.readFileSync(indexLessFilePath, 'utf8'));
pureFile.path = pureFile.path.replace('index.tsx', 'index-pure.less');
// Rewrite `index.less` file with `root-entry-name`
const indexLessFile = file.clone();
indexLessFile.contents = Buffer.from(
[
// Inject variable
'@root-entry-name: default;',
// Point to origin file
"@import './index-pure.less';",
].join('\n\n'),
);
indexLessFile.path = indexLessFile.path.replace('index.tsx', 'index.less');
return [indexLessFile, pureFile];
}
}
return [];
},
lessConfig: {
modifyVars: {
'root-entry-name': 'default',
},
},
finalize: finalizeCompile,
},
dist: {
finalize: finalizeDist,
},
generateThemeFileContent,
bail: true,
};

15
.dumi/hooks/useLocale.tsx Normal file
View File

@ -0,0 +1,15 @@
import * as React from 'react';
import { useLocale as useDumiLocale } from 'dumi';
export interface LocaleMap<Key extends string> {
cn: Record<Key, string>;
en: Record<Key, string>;
}
export default function useLocale<Key extends string>(
localeMap?: LocaleMap<Key>,
): [Record<Key, string>, 'cn' | 'en'] {
const { id } = useDumiLocale();
const localeType = id === 'zh-CN' ? 'cn' : ('en' as const);
return [localeMap?.[localeType]!, localeType];
}

View File

@ -0,0 +1,47 @@
import { useLocation as useDumiLocation } from 'dumi';
import * as React from 'react';
import useLocale from './useLocale';
function clearPath(path: string) {
return path.replace('-cn', '').replace(/\/$/, '');
}
export default function useLocation() {
const location = useDumiLocation();
const { search } = location;
const [, localeType] = useLocale();
const getLink = React.useCallback(
(path: string, hash?: string | { cn: string; en: string }) => {
let pathname = clearPath(path);
if (localeType === 'cn') {
pathname = `${pathname}-cn`;
}
if (search) {
pathname = `${pathname}${search}`;
}
if (hash) {
let hashStr: string;
if (typeof hash === 'object') {
hashStr = hash[localeType];
} else {
hashStr = hash;
}
pathname = `${pathname}#${hashStr}`;
}
return pathname;
},
[localeType, search],
);
return {
...location,
pathname: clearPath(location.pathname),
getLink,
};
}

138
.dumi/hooks/useMenu.tsx Normal file
View File

@ -0,0 +1,138 @@
import React, { ReactNode, useMemo } from 'react';
import { MenuProps } from 'antd';
import { Link, useFullSidebarData, useSidebarData } from 'dumi';
import useLocation from './useLocation';
export type UseMenuOptions = {
before?: ReactNode;
after?: ReactNode;
};
const useMenu = (options: UseMenuOptions = {}): [MenuProps['items'], string] => {
const fullData = useFullSidebarData();
const { pathname } = useLocation();
const sidebarData = useSidebarData();
const { before, after } = options;
const menuItems = useMemo<MenuProps['items']>(() => {
const sidebarItems = [...(sidebarData ?? [])];
// 将设计文档未分类的放在最后
if (pathname.startsWith('/docs/spec')) {
const notGrouped = sidebarItems.splice(0, 1);
sidebarItems.push(...notGrouped);
}
// 把 /changelog 拼到开发文档中
if (pathname.startsWith('/docs/react')) {
const changelogData = Object.entries(fullData).find(([key]) =>
key.startsWith('/changelog'),
)?.[1];
if (changelogData) {
sidebarItems.push(...changelogData);
}
}
if (pathname.startsWith('/changelog')) {
const reactDocData = Object.entries(fullData).find(([key]) =>
key.startsWith('/docs/react'),
)?.[1];
if (reactDocData) {
sidebarItems.unshift(...reactDocData);
}
}
return (
sidebarItems?.reduce<Exclude<MenuProps['items'], undefined>>((result, group) => {
if (group.title) {
// 设计文档特殊处理二级分组
if (pathname.startsWith('/docs/spec')) {
const childrenGroup = group.children.reduce<
Record<string, ReturnType<typeof useSidebarData>[number]['children']>
>((childrenResult, child) => {
const type = (child.frontmatter as any).type ?? 'default';
if (!childrenResult[type]) {
childrenResult[type] = [];
}
childrenResult[type].push(child);
return childrenResult;
}, {});
const childItems = [];
childItems.push(
...childrenGroup.default.map(item => ({
label: (
<Link to={item.link}>
{before}
{item.title}
{after}
</Link>
),
key: item.link.replace(/(-cn$)/g, ''),
})),
);
Object.entries(childrenGroup).forEach(([type, children]) => {
if (type !== 'default') {
childItems.push({
type: 'group',
label: type,
key: type,
children: children?.map(item => ({
label: (
<Link to={item.link}>
{before}
{item.title}
{after}
</Link>
),
key: item.link.replace(/(-cn$)/g, ''),
})),
});
}
});
result.push({
label: group.title,
key: group.title,
children: childItems,
});
} else {
result.push({
type: 'group',
label: group.title,
key: group.title,
children: group.children?.map(item => ({
label: (
<Link to={item.link}>
{before}
<span key="english">{item.title}</span>
<span className="chinese" key="chinese">
{(item.frontmatter as any).subtitle}
</span>
{after}
</Link>
),
key: item.link.replace(/(-cn$)/g, ''),
})),
});
}
} else {
result.push(
...group.children?.map(item => ({
label: (
<Link to={item.link}>
{before}
{item.title}
{after}
</Link>
),
key: item.link.replace(/(-cn$)/g, ''),
})),
);
}
return result;
}, []) ?? []
);
}, [sidebarData, fullData, pathname]);
return [menuItems, pathname];
};
export default useMenu;

View File

@ -0,0 +1,35 @@
import { theme } from 'antd';
import { useContext } from 'react';
import { ConfigContext } from 'antd/es/config-provider';
const { useToken } = theme;
const useSiteToken = () => {
const result = useToken();
const { getPrefixCls, iconPrefixCls } = useContext(ConfigContext);
const rootPrefixCls = getPrefixCls();
const { token } = result;
const siteMarkdownCodeBg = token.colorFillTertiary;
return {
...result,
token: {
...token,
headerHeight: 64,
menuItemBorder: 2,
mobileMaxWidth: 767.99,
siteMarkdownCodeBg,
antCls: `.${rootPrefixCls}`,
iconCls: `.${iconPrefixCls}`,
/** 56 */
marginFarXS: (token.marginXXL / 6) * 7,
/** 80 */
marginFarSM: (token.marginXXL / 3) * 5,
/** 96 */
marginFar: token.marginXXL * 1.8,
codeFamily: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace`,
},
};
};
export default useSiteToken;

View File

@ -1,20 +1,10 @@
import React, { useEffect } from 'react';
import { Link } from 'bisheng/router';
import { Result, Button } from 'antd';
import { HomeOutlined } from '@ant-design/icons';
import * as utils from './utils';
import { Link, useLocation } from 'dumi';
import * as utils from '../../theme/utils';
export interface NotFoundProps {
location: {
pathname: string;
search: string;
hash: string;
state: any;
action: string;
key: any;
basename: string;
query: Record<string, string>;
};
router: {
push: (pathname: string) => void;
replace: (pathname: string) => void;
@ -26,11 +16,8 @@ const DIRECT_MAP: Record<string, string> = {
'docs/spec/work-with-us': 'docs/resources',
};
export default function NotFound(props: NotFoundProps) {
const {
location: { pathname },
router,
} = props;
const NotFoundPage: React.FC<NotFoundProps> = ({ router }) => {
const { pathname } = useLocation();
const isZhCN = utils.isZhCN(pathname);
@ -62,11 +49,8 @@ export default function NotFound(props: NotFoundProps) {
}
/>
</section>
<style
dangerouslySetInnerHTML={{
__html: '#react-content { height: 100%; background-color: #fff }',
}}
/>
</div>
);
}
};
export default NotFoundPage;

View File

@ -0,0 +1 @@
export { default } from '../index/index';

View File

@ -0,0 +1,147 @@
import * as React from 'react';
import { Button, Space, Typography } from 'antd';
import useLocale from '../../../hooks/useLocale';
import useSiteToken from '../../../hooks/useSiteToken';
import { GroupMask } from './Group';
import { Link, useLocation } from 'dumi';
import * as utils from '../../../theme/utils';
const locales = {
cn: {
slogan: '助力设计开发者「更灵活」的搭建出「更美」的产品,让用户「快乐工作」~',
start: '开始使用',
designLanguage: '设计语言',
},
en: {
slogan:
'Help design developers "more flexible" to build "more beautiful" products, helping users to "work happily"~',
start: 'Get Start',
designLanguage: 'Design Language',
},
};
export interface BannerProps {
children?: React.ReactNode;
}
export default function Banner({ children }: BannerProps) {
const [locale, lang] = useLocale(locales);
const { pathname, search } = useLocation();
const { token } = useSiteToken();
const isZhCN = utils.isZhCN(pathname);
return (
<>
{/* Banner Placeholder Motion */}
<div
style={{
height: 240,
background: '#77C6FF',
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'center',
}}
>
<div
style={{
backgroundImage: `url(https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*6d50SboraPIAAAAAAAAAAAAAARQnAQ)`,
flex: 'auto',
backgroundRepeat: 'repeat-x',
backgroundPosition: '100% 0',
backgroundSize: 'auto 100%',
}}
/>
<video
style={{ height: '100%', objectFit: 'contain' }}
autoPlay
muted
loop
>
<source
src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*uYT7SZwhJnUAAAAAAAAAAAAADgCCAQ"
type="video/webm"
/>
<source
src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/file/A*XYYNQJ3NbmMAAAAAAAAAAAAAARQnAQ"
type="video/mp4"
/>
</video>
<div
style={{
backgroundImage: `url(https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*8ILtRrQlVDMAAAAAAAAAAAAAARQnAQ)`,
flex: 'auto',
backgroundRepeat: 'repeat-x',
backgroundPosition: '0 0',
backgroundSize: 'auto 100%',
}}
/>
</div>
{/* Logo */}
<div style={{ position: 'relative', background: '#fff' }}>
{/* Image Bottom Right */}
<img
style={{ position: 'absolute', right: 0, top: 240, width: 240 }}
src="https://gw.alipayobjects.com/zos/bmw-prod/b3b8dc41-dce8-471f-9d81-9a0204f27d03.svg"
/>
<GroupMask
style={{
textAlign: 'center',
paddingTop: token.marginFar,
paddingBottom: token.marginFarSM,
}}
>
{/* Image Left Top */}
<img
style={{ position: 'absolute', left: 0, top: 0, width: 240 }}
src="https://gw.alipayobjects.com/zos/bmw-prod/49f963db-b2a8-4f15-857a-270d771a1204.svg"
/>
{/* Image Left Top */}
<img
style={{ position: 'absolute', right: 120, top: 0, width: 240 }}
src="https://gw.alipayobjects.com/zos/bmw-prod/e152223c-bcae-4913-8938-54fda9efe330.svg"
/>
<Typography.Title
level={1}
style={{
fontFamily: `AliPuHui, ${token.fontFamily}`,
fontSize: token.fontSizes[9],
lineHeight: token.lineHeights[9],
fontWeight: 900,
marginBottom: token.marginMD,
}}
>
Ant Design 5.0
</Typography.Title>
<Typography.Paragraph
style={{
fontSize: token.fontSizeHeading5,
lineHeight: token.lineHeightHeading5,
marginBottom: token.marginMD * 2,
}}
>
<div>{locale.slogan}</div>
</Typography.Paragraph>
<Space size="middle" style={{ marginBottom: token.marginFar }}>
<Link to={utils.getLocalizedPathname('/components/overview/', isZhCN, search)}>
<Button size="large" type="primary">
{locale.start}
</Button>
</Link>
<Link to={utils.getLocalizedPathname('/docs/spec/introduce/', isZhCN, search)}>
<Button size="large">{locale.designLanguage}</Button>
</Link>
</Space>
{children}
</GroupMask>
</div>
</>
);
}

View File

@ -0,0 +1,75 @@
import * as React from 'react';
import type { Extra, Icon } from './util';
import useSiteToken from '../../../hooks/useSiteToken';
import { Col, Row, Card, Typography, Skeleton } from 'antd';
import { css } from '@emotion/react';
const useStyle = () => {
const { token } = useSiteToken();
return {
card: css`
border: ${token.lineWidth}px solid ${token.colorBorderSecondary};
border-radius: ${token.borderRadiusLG}px;
padding-block: ${token.paddingMD}px;
padding-inline: ${token.paddingLG}px;
flex: 1 1 0;
width: 33%;
display: flex;
flex-direction: column;
align-items: stretch;
text-decoration: none;
transition: all ${token.motionDurationSlow};
background: ${token.colorBgContainer};
&:hover {
box-shadow: ${token.boxShadowCard};
}
`,
};
};
export interface BannerRecommendsProps {
extras?: Extra[];
icons?: Icon[];
}
export default function BannerRecommends({ extras = [], icons = [] }: BannerRecommendsProps) {
const style = useStyle();
const first3 = extras.length === 0 ? Array(3).fill(null) : extras.slice(0, 3);
const { token } = useSiteToken();
return (
<div
style={{
maxWidth: 1208,
marginInline: 'auto',
boxSizing: 'border-box',
paddingInline: token.marginXXL,
display: 'flex',
columnGap: token.paddingMD * 2,
alignItems: 'stretch',
textAlign: 'start',
}}
>
{first3.map((extra, index) => {
if (!extra) {
return <Skeleton />;
}
const icon = icons.find((icon) => icon.name === extra.source);
return (
<a key={index} href={extra.href} target="_blank" css={style.card}>
<Typography.Title level={5}>{extra.title}</Typography.Title>
<Typography.Paragraph type="secondary" style={{ flex: 'auto' }}>
{extra.description}
</Typography.Paragraph>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography.Text>{extra.date}</Typography.Text>
{icon && <img src={icon.href} style={{ height: token.fontSize }} />}
</div>
</a>
);
})}
</div>
);
}

View File

@ -0,0 +1,260 @@
import useSiteToken from '../../../hooks/useSiteToken';
import React from 'react';
import {
Space,
Typography,
Tour,
Tag,
DatePicker,
Alert,
Modal,
FloatButton,
Progress,
ConfigProvider,
} from 'antd';
import dayjs from 'dayjs';
import { CustomerServiceOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons';
import { css } from '@emotion/react';
import useLocale from '../../../hooks/useLocale';
const SAMPLE_CONTENT_EN =
'Ant Design 5.0 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.';
const SAMPLE_CONTENT_CN =
'Ant Design 5.0 使用 CSS-in-JS 技术以提供动态与混合主题的能力。与此同时,我们使用组件级别的 CSS-in-JS 解决方案,让你的应用获得更好的性能。';
const locales = {
cn: {
yesterday: '昨天',
lastWeek: '上周',
lastMonth: '上月',
lastYear: '去年',
new: '新增',
update: '更新',
sampleContent: SAMPLE_CONTENT_CN,
inProgress: '进行中',
success: '成功',
taskFailed: '任务失败',
tour: '漫游导览帮助用户对新加的功能进行快速了解',
},
en: {
yesterday: 'Yesterday',
lastWeek: 'Last Week',
lastMonth: 'Last Month',
lastYear: 'Last Year',
new: 'New',
update: 'Update',
sampleContent: SAMPLE_CONTENT_EN,
inProgress: 'In Progress',
success: 'Success',
taskFailed: 'Task Failed',
tour: 'A quick guide for new come user about how to use app.',
},
};
const useStyle = () => {
const { token } = useSiteToken();
return {
card: css`
border-radius: ${token.borderRadius}px;
background: #f5f8ff;
padding: ${token.paddingXL}px;
flex: none;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
align-items: stretch;
> * {
flex: none;
}
`,
cardCircle: css`
position: absolute;
width: 120px;
height: 120px;
background: #1677ff;
border-radius: 50%;
filter: blur(40px);
opacity: 0.1;
`,
};
};
export default function ComponentsList() {
const { token } = useSiteToken();
const styles = useStyle();
const [locale] = useLocale(locales);
const COMPONENTS: {
title: React.ReactNode;
type: 'new' | 'update';
node: React.ReactNode;
}[] = React.useMemo(
() => [
{
title: 'Modal',
type: 'update',
node: (
<Modal._InternalPanelDoNotUseOrYouWillBeFired title="Ant Design 5.0" width={300}>
{locale.sampleContent}
</Modal._InternalPanelDoNotUseOrYouWillBeFired>
),
},
{
title: 'DatePicker',
type: 'update',
node: (
<DatePicker._InternalPanelDoNotUseOrYouWillBeFired
showToday={false}
presets={[
{ label: locale.yesterday, value: dayjs().add(-1, 'd') },
{ label: locale.lastWeek, value: dayjs().add(-7, 'd') },
{ label: locale.lastMonth, value: dayjs().add(-1, 'month') },
{ label: locale.lastYear, value: dayjs().add(-1, 'year') },
]}
value={dayjs('2022-11-18 14:00:00')}
/>
),
},
{
title: 'Progress',
type: 'update',
node: (
<Space direction="vertical">
<Space>
<Progress type="circle" trailColor="#e6f4ff" percent={60} width={14} />
{locale.inProgress}
</Space>
<Space>
<Progress type="circle" percent={100} width={14} />
{locale.success}
</Space>
<Space>
<Progress type="circle" status="exception" percent={88} width={14} />
{locale.taskFailed}
</Space>
</Space>
),
},
{
title: 'Tour',
type: 'new',
node: (
<Tour._InternalPanelDoNotUseOrYouWillBeFired
title="Ant Design 5.0"
description={locale.tour}
style={{ width: 350 }}
current={3}
total={9}
/>
),
},
{
title: 'FloatButton',
type: 'new',
node: (
<Space size="large">
<FloatButton._InternalPanelDoNotUseOrYouWillBeFired
shape="square"
items={[
{
icon: <QuestionCircleOutlined />,
},
{
icon: <CustomerServiceOutlined />,
},
{
icon: <SyncOutlined />,
},
]}
/>
<FloatButton._InternalPanelDoNotUseOrYouWillBeFired backTop />
<FloatButton._InternalPanelDoNotUseOrYouWillBeFired
items={[
{
icon: <QuestionCircleOutlined />,
},
{
icon: <CustomerServiceOutlined />,
},
{
icon: <SyncOutlined />,
},
]}
/>
</Space>
),
},
// {
// title: 'Steps',
// type: 'update',
// node: <Button style={{ width: PLACEHOLDER_WIDTH }}>Placeholder</Button>,
// },
{
title: 'Alert',
type: 'update',
node: (
<Alert
style={{ width: 400 }}
message="Ant Design 5.0"
description={locale.sampleContent}
closable
/>
),
},
],
[],
);
return (
<div style={{ width: '100%', overflow: 'hidden', display: 'flex', justifyContent: 'center' }}>
<div style={{ display: 'flex', alignItems: 'stretch', columnGap: token.paddingLG }}>
{COMPONENTS.map(({ title, node, type }, index) => {
const tagColor = type === 'new' ? 'processing' : 'warning';
const tagText = type === 'new' ? locale.new : locale.update;
return (
<div key={index} css={styles.card} style={{ pointerEvents: 'none' }}>
{/* Decorator */}
<div
css={styles.cardCircle}
style={{
right: (index % 2) * -20 - 20,
bottom: (index % 3) * -40 - 20,
}}
/>
{/* Title */}
<Space>
<Typography.Title level={4} style={{ fontWeight: 'normal', margin: 0 }}>
{title}
</Typography.Title>
<Tag color={tagColor}>{tagText}</Tag>
</Space>
<div
style={{
marginTop: token.paddingLG,
flex: 'auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{node}
</div>
</div>
);
})}
</div>
</div>
);
}

View File

@ -0,0 +1,171 @@
import useSiteToken from '../../../hooks/useSiteToken';
import { Col, Row, Typography } from 'antd';
import React from 'react';
import { css } from '@emotion/react';
import useLocale from '../../../hooks/useLocale';
import { Link, useLocation } from 'dumi';
import * as utils from '../../../theme/utils';
const SECONDARY_LIST = [
{
img: 'https://gw.alipayobjects.com/zos/bmw-prod/b874caa9-4458-412a-9ac6-a61486180a62.svg',
key: 'mobile',
url: 'https://mobile.ant.design/',
imgScale: 1.5,
},
{
img: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
key: 'antv',
url: 'https://antv.vision/',
},
{
img: 'https://gw.alipayobjects.com/zos/bmw-prod/af1ea898-bf02-45d1-9f30-8ca851c70a5b.svg',
key: 'kitchen',
url: 'https://kitchen.alipay.com/',
},
];
const locales = {
cn: {
values: '设计价值观',
valuesDesc: '确定性、意义感、生长性、自然',
guide: '设计指引',
guideDesc: '全局样式、设计模式',
lib: '组件库',
libDesc: 'Ant Design of React / Angular / Vue',
// Secondary
mobile: 'Ant Design Mobile',
mobileDesc: 'Ant Design 移动端 UI 组件库',
antv: 'AntV',
antvDesc: '全新一代数据可视化解决方案',
kitchen: 'Kitchen',
kitchenDesc: '一款为设计者提升工作效率的 Sketch 工具集',
},
en: {
values: 'Design values',
valuesDesc: 'Certainty, Meaningfulness, Growth, Naturalness',
guide: 'Design guide',
guideDesc: 'Global style and design pattern',
lib: 'Components Libraries',
libDesc: 'Ant Design of React / Angular / Vue',
// Secondary
mobile: 'Ant Design Mobile',
mobileDesc: 'Mobile UI component library',
antv: 'AntV',
antvDesc: 'New generation of data visualization solutions',
kitchen: 'Kitchen',
kitchenDesc: 'Sketch Tool set for designers',
},
};
const useStyle = () => {
const { token } = useSiteToken();
return {
card: css`
padding: ${token.paddingSM}px;
border-radius: ${token.borderRadius * 2}px;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02),
0 2px 4px rgba(0, 0, 0, 0.02);
img {
width: 100%;
vertical-align: top;
border-radius: ${token.borderRadius}px;
}
`,
cardMini: css`
display: block;
border-radius: ${token.borderRadius * 2}px;
padding: ${token.paddingMD}px ${token.paddingLG}px;
background: rgba(0, 0, 0, 0.02);
border: 1px solid rgba(0, 0, 0, 0.06);
img {
height: 48px;
}
`,
};
};
export default function DesignFramework() {
const [locale] = useLocale(locales);
const { token } = useSiteToken();
const style = useStyle();
const { pathname, search } = useLocation();
const isZhCN = utils.isZhCN(pathname);
const MAINLY_LIST = [
{
img: 'https://gw.alipayobjects.com/zos/bmw-prod/36a89a46-4224-46e2-b838-00817f5eb364.svg',
key: 'values',
path: utils.getLocalizedPathname('/docs/spec/values/', isZhCN, search),
},
{
img: 'https://gw.alipayobjects.com/zos/bmw-prod/8379430b-e328-428e-8a67-666d1dd47f7d.svg',
key: 'guide',
path: utils.getLocalizedPathname('/docs/spec/colors/', isZhCN, search),
},
{
img: 'https://gw.alipayobjects.com/zos/bmw-prod/1c363c0b-17c6-4b00-881a-bc774df1ebeb.svg',
key: 'lib',
path: utils.getLocalizedPathname('/docs/react/introduce/', isZhCN, search),
},
];
return (
<Row gutter={[token.marginXL, token.marginXL]}>
{MAINLY_LIST.map(({ img, key, path }, index) => {
const title = locale[key as keyof typeof locale];
const desc = locale[`${key}Desc` as keyof typeof locale];
return (
<Col key={index} span={8}>
<Link to={path}>
<div css={style.card}>
<img alt={title} src={img} />
<Typography.Title
level={4}
style={{ marginTop: token.margin, marginBottom: token.marginXS }}
>
{title}
</Typography.Title>
<Typography.Paragraph type="secondary" style={{ margin: 0 }}>
{desc}
</Typography.Paragraph>
</div>
</Link>
</Col>
);
})}
{SECONDARY_LIST.map(({ img, key, url, imgScale = 1 }, index) => {
const title = locale[key as keyof typeof locale];
const desc = locale[`${key}Desc` as keyof typeof locale];
return (
<Col key={index} span={8}>
<a css={style.cardMini} target="_blank" href={url}>
<img alt={title} src={img} style={{ transform: `scale(${imgScale})` }} />
<Typography.Title
level={4}
style={{ marginTop: token.margin, marginBottom: token.marginXS }}
>
{title}
</Typography.Title>
<Typography.Paragraph type="secondary" style={{ margin: 0 }}>
{desc}
</Typography.Paragraph>
</a>
</Col>
);
})}
</Row>
);
}

View File

@ -0,0 +1,108 @@
import * as React from 'react';
import { Typography } from 'antd';
import useSiteToken from '../../../hooks/useSiteToken';
export interface GroupMaskProps {
style?: React.CSSProperties;
children?: React.ReactNode;
disabled?: boolean;
}
export function GroupMask({ children, style, disabled }: GroupMaskProps) {
const additionalStyle: React.CSSProperties = disabled
? {}
: {
position: 'relative',
background: `rgba(255,255,255,0.1)`,
backdropFilter: `blur(25px)`,
zIndex: 1,
};
return (
<div
className="site-mask"
style={{
position: 'relative',
...style,
...additionalStyle,
}}
>
{children}
</div>
);
}
export interface GroupProps {
id?: string;
title?: React.ReactNode;
titleColor?: string;
description?: React.ReactNode;
children?: React.ReactNode;
background?: string;
/** 是否不使用两侧 margin */
collapse?: boolean;
decoration?: React.ReactNode;
}
export default function Group(props: GroupProps) {
const { id, title, titleColor, description, children, decoration, background, collapse } = props;
const { token } = useSiteToken();
const marginStyle: React.CSSProperties = collapse
? {}
: {
maxWidth: 1208,
marginInline: 'auto',
boxSizing: 'border-box',
paddingInline: token.marginXXL,
};
let childNode = (
<>
<div style={{ textAlign: 'center' }}>
<Typography.Title
id={id}
level={1}
style={{
fontWeight: 900,
color: titleColor,
// Special for the title
fontFamily: `AliPuHui, ${token.fontFamily}`,
}}
>
{title}
</Typography.Title>
<Typography.Paragraph style={{ marginBottom: token.marginFarXS, color: titleColor }}>
{description}
</Typography.Paragraph>
</div>
<div style={marginStyle}>
{children ? (
<div>{children}</div>
) : (
<div
style={{ borderRadius: token.borderRadiusLG, minHeight: 300, background: '#e9e9e9' }}
/>
)}
</div>
</>
);
return (
<div
style={{ position: 'relative', background, transition: `all ${token.motionDurationSlow}` }}
>
<div style={{ position: 'absolute', inset: 0 }}>{decoration}</div>
<GroupMask
disabled={!!background}
style={{
paddingBlock: token.marginFarSM,
}}
>
{childNode}
</GroupMask>
</div>
);
}

View File

@ -0,0 +1,104 @@
import * as React from 'react';
import { Row, Col, Typography } from 'antd';
import type { Recommendation } from './util';
import { css } from '@emotion/react';
import useSiteToken from '../../../hooks/useSiteToken';
const useStyle = () => {
const { token } = useSiteToken();
return {
card: css`
height: 300px;
background-size: 100% 100%;
background-position: center;
position: relative;
overflow: hidden;
&:before {
position: absolute;
background: linear-gradient(
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.25) 40%,
rgba(0, 0, 0, 0.65) 100%
);
opacity: 0.3;
transition: all 0.5s;
content: '';
pointer-events: none;
inset: 0;
}
&:hover {
&:before {
opacity: 1;
}
.intro {
transform: translateY(0);
h4${token.antCls}-typography {
padding-bottom: 0;
}
}
}
.intro {
position: absolute;
right: 0;
bottom: 0;
left: 0;
transform: translateY(100%);
transition: all ${token.motionDurationSlow};
${token.antCls}-typography {
margin: 0;
color: #fff;
font-weight: normal;
text-shadow: 0 0 1px rgba(0, 0, 0, 0.5);
transition: all ${token.motionDurationSlow};
}
h4${token.antCls}-typography {
position: absolute;
padding: 0 ${token.paddingMD}px ${token.paddingMD}px;
transform: translateY(-100%);
}
div${token.antCls}-typography {
padding: ${token.paddingXS}px ${token.paddingMD}px ${token.paddingLG}px;
}
}
`,
};
};
export interface RecommendsProps {
recommendations?: Recommendation[];
}
export default function Recommends({ recommendations = [] }: RecommendsProps) {
const { token } = useSiteToken();
const style = useStyle();
return (
<Row gutter={token.marginLG}>
{new Array(3).fill(null).map((_, index) => {
const data = recommendations[index];
return (
<Col key={index} span={8}>
{data ? (
<div css={style.card} style={{ backgroundImage: `url(${data.img})` }}>
<div className="intro">
<Typography.Title level={4}>{data.title}</Typography.Title>
<Typography.Paragraph>{data.description}</Typography.Paragraph>
</div>
</div>
) : null}
</Col>
);
})}
</Row>
);
}

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import useSiteToken from '../../../../hooks/useSiteToken';
import { COLOR_IMAGES, DEFAULT_COLOR, getClosetColor } from './colorUtil';
export interface BackgroundImageProps {
colorPrimary?: string;
isLight?: boolean;
}
export default function BackgroundImage({ colorPrimary, isLight }: BackgroundImageProps) {
const { token } = useSiteToken();
const activeColor = React.useMemo(() => getClosetColor(colorPrimary), [colorPrimary]);
const sharedStyle: React.CSSProperties = {
transition: `all ${token.motionDurationSlow}`,
position: 'absolute',
left: 0,
top: 0,
height: '100%',
width: '100%',
};
return (
<>
{COLOR_IMAGES.map(({ color, url }) => {
if (!url) {
return null;
}
return (
<img
key={color}
style={{
...sharedStyle,
opacity: isLight && activeColor === color ? 1 : 0,
objectFit: 'cover',
objectPosition: 'right top',
}}
src={url}
/>
);
})}
{/* <div
style={{
...sharedStyle,
opacity: isLight || !activeColor || activeColor === DEFAULT_COLOR ? 0 : 1,
background: 'rgba(0,0,0,0.79)',
}}
/> */}
</>
);
}

View File

@ -0,0 +1,127 @@
import useSiteToken from '../../../../hooks/useSiteToken';
import { Input, Space, Popover } from 'antd';
import React, { FC, useEffect, useState } from 'react';
import { css } from '@emotion/react';
import { TinyColor } from '@ctrl/tinycolor';
import { PRESET_COLORS } from './colorUtil';
import ColorPanel, { ColorPanelProps } from 'antd-token-previewer/es/ColorPanel';
const useStyle = () => {
const { token } = useSiteToken();
return {
color: css`
width: ${token.controlHeightLG / 2}px;
height: ${token.controlHeightLG / 2}px;
border-radius: 100%;
cursor: pointer;
transition: all ${token.motionDurationFast};
`,
colorActive: css`
box-shadow: 0 0 0 1px ${token.colorBgContainer},
0 0 0 ${token.controlOutlineWidth * 2 + 1}px ${token.colorPrimary};
`,
};
};
const DebouncedColorPanel: FC<ColorPanelProps> = ({ color, onChange }) => {
const [value, setValue] = useState(color);
useEffect(() => {
const timeout = setTimeout(() => {
onChange?.(value);
}, 200);
return () => clearTimeout(timeout);
}, [value]);
useEffect(() => {
setValue(color);
}, [color]);
return <ColorPanel color={value} onChange={setValue} />;
};
export interface RadiusPickerProps {
value?: string;
onChange?: (value: string) => void;
}
export default function ColorPicker({ value, onChange }: RadiusPickerProps) {
const style = useStyle();
const matchColors = React.useMemo(() => {
const valueStr = new TinyColor(value).toRgbString();
let existActive = false;
const colors = PRESET_COLORS.map(color => {
const colorStr = new TinyColor(color).toRgbString();
const active = colorStr === valueStr;
existActive = existActive || active;
return {
color,
active,
picker: false,
};
});
return [
...colors,
{
color: 'conic-gradient(red, yellow, lime, aqua, blue, magenta, red)',
picker: true,
active: !existActive,
},
];
}, [value]);
return (
<Space size="large">
<Input
value={value}
onChange={event => {
onChange?.(event.target.value);
}}
style={{ width: 120 }}
/>
<Space size="middle">
{matchColors.map(({ color, active, picker }) => {
let colorNode = (
<div
key={color}
css={[style.color, active && style.colorActive]}
style={{
background: color,
}}
onClick={() => {
if (!picker) {
onChange?.(color);
}
}}
/>
);
if (picker) {
colorNode = (
<Popover
key={color}
overlayInnerStyle={{ padding: 0 }}
content={
<DebouncedColorPanel color={value || ''} onChange={color => onChange?.(color)} />
}
trigger="click"
showArrow={false}
>
{colorNode}
</Popover>
);
}
return colorNode;
})}
</Space>
</Space>
);
}

View File

@ -0,0 +1,31 @@
import { InputNumber, Space, Slider } from 'antd';
import React from 'react';
export interface RadiusPickerProps {
value?: number;
onChange?: (value: number | null) => void;
}
export default function RadiusPicker({ value, onChange }: RadiusPickerProps) {
return (
<Space size="large">
<InputNumber
value={value}
onChange={onChange}
style={{ width: 120 }}
min={0}
formatter={val => `${val}px`}
parser={str => (str ? parseFloat(str) : (str as any))}
/>
<Slider
tooltip={{ open: false }}
style={{ width: 128 }}
min={0}
value={value}
max={20}
onChange={onChange}
/>
</Space>
);
}

View File

@ -0,0 +1,96 @@
import { css } from '@emotion/react';
import { Space } from 'antd';
import * as React from 'react';
import useSiteToken from '../../../../hooks/useSiteToken';
import useLocale from '../../../../hooks/useLocale';
export const THEMES = {
default: 'https://gw.alipayobjects.com/zos/bmw-prod/ae669a89-0c65-46db-b14b-72d1c7dd46d6.svg',
dark: 'https://gw.alipayobjects.com/zos/bmw-prod/0f93c777-5320-446b-9bb7-4d4b499f346d.svg',
lark: 'https://gw.alipayobjects.com/zos/bmw-prod/3e899b2b-4eb4-4771-a7fc-14c7ff078aed.svg',
comic: 'https://gw.alipayobjects.com/zos/bmw-prod/ed9b04e8-9b8d-4945-8f8a-c8fc025e846f.svg',
} as const;
export type THEME = keyof typeof THEMES;
const locales = {
cn: {
default: '默认',
dark: '暗黑',
lark: '知识协作',
comic: '桃花缘',
},
en: {
default: 'Default',
dark: 'Dark',
lark: 'Document',
comic: 'Blossom',
},
};
const useStyle = () => {
const { token } = useSiteToken();
return {
themeCard: css`
border-radius: ${token.borderRadius}px;
cursor: pointer;
transition: all ${token.motionDurationSlow};
overflow: hidden;
img {
vertical-align: top;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
}
&:hover {
transform: scale(1.04);
}
`,
themeCardActive: css`
box-shadow: 0 0 0 1px ${token.colorBgContainer},
0 0 0 ${token.controlOutlineWidth * 2 + 1}px ${token.colorPrimary};
&,
&:hover {
transform: scale(1);
}
`,
};
};
export interface ThemePickerProps {
value?: string;
onChange?: (value: string) => void;
}
export default function ThemePicker({ value, onChange }: ThemePickerProps) {
const { token } = useSiteToken();
const style = useStyle();
const [locale] = useLocale(locales);
return (
<Space size={token.paddingLG}>
{Object.keys(THEMES).map(theme => {
const url = THEMES[theme as THEME];
return (
<Space key={theme} direction="vertical" align="center">
<div css={[style.themeCard, value === theme && style.themeCardActive]}>
<img
src={url}
onClick={() => {
onChange?.(theme);
}}
/>
</div>
<span>{locale[theme as keyof typeof locale]}</span>
</Space>
);
})}
</Space>
);
}

View File

@ -0,0 +1,79 @@
import { TinyColor } from '@ctrl/tinycolor';
export const DEFAULT_COLOR = '#1677FF';
export const PINK_COLOR = '#ED4192';
export const COLOR_IMAGES = [
{
color: DEFAULT_COLOR,
// url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*QEAoSL8uVi4AAAAAAAAAAAAAARQnAQ',
url: null,
},
{
color: '#5A54F9',
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*MtVDSKukKj8AAAAAAAAAAAAAARQnAQ',
},
{
color: '#9E339F',
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*FMluR4vJhaQAAAAAAAAAAAAAARQnAQ',
},
{
color: PINK_COLOR,
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*DGZXS4YOGp0AAAAAAAAAAAAAARQnAQ',
},
{
color: '#E0282E',
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*w6xcR7MriwEAAAAAAAAAAAAAARQnAQ',
},
{
color: '#F4801A',
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*VWFOTbEyU9wAAAAAAAAAAAAAARQnAQ',
},
{
color: '#F2BD27',
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*1yydQLzw5nYAAAAAAAAAAAAAARQnAQ',
},
{
color: '#00B96B',
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*XpGeRoZKGycAAAAAAAAAAAAAARQnAQ',
},
] as const;
export const PRESET_COLORS = COLOR_IMAGES.map(({ color }) => color);
const DISTANCE = 33;
export function getClosetColor(colorPrimary?: string | null) {
if (!colorPrimary) {
return null;
}
const colorPrimaryRGB = new TinyColor(colorPrimary).toRgb();
const distance = COLOR_IMAGES.map(({ color }) => {
const colorObj = new TinyColor(color).toRgb();
const dist = Math.sqrt(
Math.pow(colorObj.r - colorPrimaryRGB.r, 2) +
Math.pow(colorObj.g - colorPrimaryRGB.g, 2) +
Math.pow(colorObj.b - colorPrimaryRGB.b, 2),
);
return { color, dist };
});
const firstMatch = distance.sort((a, b) => a.dist - b.dist)[0];
return firstMatch.dist <= DISTANCE ? firstMatch.color : null;
}
export function getAvatarURL(color?: string | null) {
const closestColor = getClosetColor(color);
if (!closestColor) {
return null;
}
return (
COLOR_IMAGES.find(obj => obj.color === closestColor)?.url ||
'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*CLp0Qqc11AkAAAAAAAAAAAAAARQnAQ'
);
}

View File

@ -0,0 +1,561 @@
import * as React from 'react';
import { css } from '@emotion/react';
import { TinyColor } from '@ctrl/tinycolor';
import {
HomeOutlined,
FolderOutlined,
BellOutlined,
QuestionCircleOutlined,
} from '@ant-design/icons';
import useLocale from '../../../../hooks/useLocale';
import useSiteToken from '../../../../hooks/useSiteToken';
import {
Typography,
Layout,
Menu,
Breadcrumb,
MenuProps,
Space,
ConfigProvider,
Card,
Form,
Radio,
theme,
Button,
} from 'antd';
import ThemePicker, { THEME } from './ThemePicker';
import ColorPicker from './ColorPicker';
import RadiusPicker from './RadiusPicker';
import Group from '../Group';
import BackgroundImage from './BackgroundImage';
import { getClosetColor, DEFAULT_COLOR, getAvatarURL, PINK_COLOR } from './colorUtil';
const { Header, Content, Sider } = Layout;
const TokenChecker = () => {
if (process.env.NODE_ENV !== 'production') {
console.log('Demo Token:', theme.useToken());
}
return null;
};
// ============================= Theme =============================
const locales = {
cn: {
themeTitle: '定制主题,随心所欲',
themeDesc: 'Ant Design 5.0 开放更多样式算法,让你定制主题更简单',
customizeTheme: '定制主题',
myTheme: '我的主题',
titlePrimaryColor: '主色',
titleBorderRadius: '圆角',
titleCompact: '宽松度',
default: '默认',
compact: '紧凑',
titleTheme: '主题',
light: '亮色',
dark: '暗黑',
toDef: '深度定制',
toUse: '去使用',
},
en: {
themeTitle: 'Flexible theme customization',
themeDesc: 'Ant Design 5.0 enable extendable algorithm, make custom theme easier',
customizeTheme: 'Customize Theme',
myTheme: 'My Theme',
titlePrimaryColor: 'Primary Color',
titleBorderRadius: 'Border Radius',
titleCompact: 'Compact',
titleTheme: 'Theme',
default: 'Default',
compact: 'Compact',
light: 'Light',
dark: 'Dark',
toDef: 'More',
toUse: 'Apply',
},
};
// ============================= Style =============================
const useStyle = () => {
const { token } = useSiteToken();
return {
demo: css`
overflow: hidden;
background: rgba(240, 242, 245, 0.25);
backdrop-filter: blur(50px);
box-shadow: 0 2px 10px 2px rgba(0, 0, 0, 0.1);
transition: all ${token.motionDurationSlow};
`,
otherDemo: css`
backdrop-filter: blur(10px);
background: rgba(247, 247, 247, 0.5);
`,
darkDemo: css`
background: #000;
`,
larkDemo: css`
// background: #f7f7f7;
background: rgba(240, 242, 245, 0.65);
`,
comicDemo: css`
// background: #ffe4e6;
background: rgba(240, 242, 245, 0.65);
`,
menu: css`
margin-left: auto;
`,
darkSideMenu: css``,
header: css`
display: flex;
align-items: center;
border-bottom: 1px solid ${token.colorSplit};
padding-inline: ${token.paddingLG}px !important;
height: ${token.controlHeightLG * 1.2}px;
line-height: ${token.controlHeightLG * 1.2}px;
`,
headerDark: css`
border-bottom-color: rgba(255, 255, 255, 0.1);
`,
avatar: css`
width: ${token.controlHeight}px;
height: ${token.controlHeight}px;
border-radius: 100%;
background: rgba(240, 240, 240, 0.75);
`,
avatarDark: css`
background: rgba(200, 200, 200, 0.3);
`,
logo: css`
display: flex;
align-items: center;
column-gap: ${token.padding}px;
h1 {
font-weight: 400;
font-size: 16px;
line-height: 1.5;
}
`,
logoImg: css`
width: 30px;
height: 30px;
overflow: hidden;
img {
width: 30px;
height: 30px;
vertical-align: top;
}
`,
logoImgPureColor: css`
img {
transform: translateX(-30px);
}
`,
transBg: css`
background: transparent !important;
`,
form: css`
width: 800px;
margin: 0 auto;
`,
};
};
interface PickerProps {
title: React.ReactNode;
}
// ========================== Menu Config ==========================
const subMenuItems: MenuProps['items'] = [
{
key: `Design Values`,
label: `Design Values`,
},
{
key: `Global Styles`,
label: `Global Styles`,
},
{
key: `Themes`,
label: `Themes`,
},
{
key: `DesignPatterns`,
label: `Design Patterns`,
},
];
const sideMenuItems: MenuProps['items'] = [
{
key: `Design`,
label: `Design`,
icon: <FolderOutlined />,
children: subMenuItems,
},
{
key: `Development`,
label: `Development`,
icon: <FolderOutlined />,
},
];
// ============================= Theme =============================
function getTitleColor(colorPrimary: string, isLight?: boolean) {
if (!isLight) {
return '#FFF';
}
const color = new TinyColor(colorPrimary);
const closestColor = getClosetColor(colorPrimary);
switch (closestColor) {
case DEFAULT_COLOR:
case PINK_COLOR:
case '#F2BD27':
return undefined;
default:
return color.toHsl().l < 0.7 ? '#FFF' : undefined;
}
}
interface ThemeData {
themeType: THEME;
colorPrimary: string;
borderRadius: number;
compact: 'default' | 'compact';
}
const ThemeDefault: ThemeData = {
themeType: 'default',
colorPrimary: '#1677FF',
borderRadius: 6,
compact: 'default',
};
const ThemesInfo: Record<THEME, Partial<ThemeData>> = {
default: {},
dark: {
borderRadius: 2,
},
lark: {
colorPrimary: '#00B96B',
borderRadius: 4,
},
comic: {
colorPrimary: PINK_COLOR,
borderRadius: 16,
},
};
export default function Theme() {
const style = useStyle();
const { token } = useSiteToken();
const [locale] = useLocale(locales);
const [themeData, setThemeData] = React.useState<ThemeData>(ThemeDefault);
const onThemeChange = (_: Partial<ThemeData>, nextThemeData: ThemeData) => {
setThemeData(nextThemeData);
};
const { compact, themeType, ...themeToken } = themeData;
const isLight = themeType !== 'dark';
const [form] = Form.useForm();
// const algorithmFn = isLight ? theme.defaultAlgorithm : theme.darkAlgorithm;
const algorithmFn = React.useMemo(() => {
const algorithms = [isLight ? theme.defaultAlgorithm : theme.darkAlgorithm];
if (compact === 'compact') {
algorithms.push(theme.compactAlgorithm);
}
return algorithms;
}, [isLight, compact]);
// ================================ Themes ================================
React.useEffect(() => {
const mergedData = {
...ThemeDefault,
themeType,
...ThemesInfo[themeType],
} as any;
setThemeData(mergedData);
form.setFieldsValue(mergedData);
}, [themeType]);
// ================================ Tokens ================================
const closestColor = getClosetColor(themeData.colorPrimary);
const [backgroundColor, avatarColor] = React.useMemo(() => {
let bgColor = 'transparent';
const mapToken = theme.defaultAlgorithm({
...theme.defaultConfig.token,
colorPrimary: themeData.colorPrimary,
});
if (themeType === 'dark') {
bgColor = '#393F4A';
} else if (closestColor === DEFAULT_COLOR) {
bgColor = '#F5F8FF';
} else {
bgColor = mapToken.colorPrimaryHover;
}
return [bgColor, mapToken.colorPrimaryBgHover];
}, [themeType, closestColor, themeData.colorPrimary]);
const logoColor = React.useMemo(() => {
const hsl = new TinyColor(themeData.colorPrimary).toHsl();
hsl.l = Math.min(hsl.l, 0.7);
return new TinyColor(hsl).toHexString();
}, [themeData.colorPrimary]);
// ================================ Render ================================
const themeNode = (
<ConfigProvider
theme={{
token: {
...themeToken,
...(isLight
? {}
: {
// colorBgContainer: '#474C56',
// colorBorderSecondary: 'rgba(255,255,255,0.06)',
}),
},
hashed: true,
algorithm: algorithmFn,
components: {
Slider: {
// 1677FF
},
Card: isLight
? {}
: {
// colorBgContainer: '#474C56',
},
Layout: isLight
? {
colorBgHeader: 'transparent',
colorBgBody: 'transparent',
}
: {
// colorBgBody: 'transparent',
},
Menu: isLight
? {
colorItemBg: 'transparent',
colorSubItemBg: 'transparent',
colorActiveBarWidth: 0,
}
: {
// colorItemBg: 'transparent',
// colorSubItemBg: 'transparent',
// colorItemBgActive: 'rgba(255,255,255,0.2)',
// colorItemBgSelected: 'rgba(255,255,255,0.2)',
},
},
}}
>
<TokenChecker />
<div
css={[
style.demo,
isLight && closestColor !== DEFAULT_COLOR && style.otherDemo,
!isLight && style.darkDemo,
]}
style={{ borderRadius: themeData.borderRadius }}
>
<Layout css={style.transBg}>
<Header css={[style.header, style.transBg, !isLight && style.headerDark]}>
{/* Logo */}
<div css={style.logo}>
<div css={[style.logoImg, closestColor !== DEFAULT_COLOR && style.logoImgPureColor]}>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
style={{
filter:
closestColor === DEFAULT_COLOR
? undefined
: `drop-shadow(30px 0 0 ${logoColor})`,
}}
/>
</div>
<h1>Ant Design 5.0</h1>
</div>
<Space css={style.menu} size="middle">
<BellOutlined />
<QuestionCircleOutlined />
<div
css={[style.avatar, themeType === 'dark' && style.avatarDark]}
style={{
backgroundColor: avatarColor,
backgroundImage: `url(${getAvatarURL(closestColor)})`,
backgroundSize: 'cover',
boxShadow: `0 0 2px rgba(0, 0, 0, 0.2)`,
}}
/>
</Space>
</Header>
<Layout css={style.transBg}>
<Sider css={style.transBg} width={200} className="site-layout-background">
<Menu
mode="inline"
css={[style.transBg, !isLight && style.darkSideMenu]}
selectedKeys={['Themes']}
openKeys={['Design']}
style={{ height: '100%', borderRight: 0 }}
items={sideMenuItems}
/>
</Sider>
<Layout css={style.transBg} style={{ padding: '0 24px 24px' }}>
<Breadcrumb style={{ margin: '16px 0' }}>
<Breadcrumb.Item>
<HomeOutlined />
</Breadcrumb.Item>
<Breadcrumb.Item overlay={<Menu items={subMenuItems} />}>Design</Breadcrumb.Item>
<Breadcrumb.Item>Themes</Breadcrumb.Item>
</Breadcrumb>
<Content>
<Typography.Title level={2}>{locale.customizeTheme}</Typography.Title>
<Card
title={locale.myTheme}
extra={
<Space>
<Button type="default">{locale.toDef}</Button>
<Button type="primary">{locale.toUse}</Button>
</Space>
}
>
<Form
form={form}
initialValues={themeData}
onValuesChange={onThemeChange}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
css={style.form}
>
<Form.Item label={locale.titleTheme} name="themeType">
<ThemePicker />
</Form.Item>
<Form.Item label={locale.titlePrimaryColor} name="colorPrimary">
<ColorPicker />
</Form.Item>
<Form.Item label={locale.titleBorderRadius} name="borderRadius">
<RadiusPicker />
</Form.Item>
<Form.Item label={locale.titleCompact} name="compact">
<Radio.Group>
<Radio value="default">{locale.default}</Radio>
<Radio value="compact">{locale.compact}</Radio>
</Radio.Group>
</Form.Item>
</Form>
</Card>
</Content>
</Layout>
</Layout>
</Layout>
</div>
</ConfigProvider>
);
const posStyle: React.CSSProperties = {
position: 'absolute',
};
return (
<Group
title={locale.themeTitle}
titleColor={getTitleColor(themeData.colorPrimary, isLight)}
description={locale.themeDesc}
id="flexible"
background={backgroundColor}
decoration={
// =========================== Theme Background ===========================
<>
{/* >>>>>> Default <<<<<< */}
<div
style={{
transition: `all ${token.motionDurationSlow}`,
opacity: isLight && closestColor === DEFAULT_COLOR ? 1 : 0,
}}
>
{/* Image Left Top */}
<img
style={{
...posStyle,
left: '50%',
transform: 'translateX(-900px)',
top: -100,
height: 500,
}}
src="https://gw.alipayobjects.com/zos/bmw-prod/bd71b0c6-f93a-4e52-9c8a-f01a9b8fe22b.svg"
/>
{/* Image Right Bottom */}
<img
style={{
...posStyle,
right: '50%',
transform: 'translateX(750px)',
bottom: -100,
height: 287,
}}
src="https://gw.alipayobjects.com/zos/bmw-prod/84ad805a-74cb-4916-b7ba-9cdc2bdec23a.svg"
/>
</div>
{/* >>>>>> Dark <<<<<< */}
<div
style={{
transition: `all ${token.motionDurationSlow}`,
opacity: !isLight || !closestColor ? 1 : 0,
}}
>
{/* Image Left Top */}
<img
style={{ ...posStyle, left: 0, top: -100, height: 500 }}
src="https://gw.alipayobjects.com/zos/bmw-prod/a213184a-f212-4afb-beec-1e8b36bb4b8a.svg"
/>
{/* Image Right Bottom */}
<img
style={{ ...posStyle, right: 0, bottom: -100, height: 287 }}
src="https://gw.alipayobjects.com/zos/bmw-prod/bb74a2fb-bff1-4d0d-8c2d-2ade0cd9bb0d.svg"
/>
</div>
{/* >>>>>> Background Image <<<<<< */}
<BackgroundImage isLight={isLight} colorPrimary={themeData.colorPrimary} />
</>
}
>
{themeNode}
</Group>
);
}

104
.dumi/pages/index/index.tsx Normal file
View File

@ -0,0 +1,104 @@
import React, { useEffect, useLayoutEffect, type FC } from 'react';
import { useLocale as useDumiLocale } from 'dumi';
import { css } from '@emotion/react';
import useLocale from '../../hooks/useLocale';
import Banner from './components/Banner';
import Group from './components/Group';
import { useSiteData } from './components/util';
import useSiteToken from '../../hooks/useSiteToken';
import Theme from './components/Theme';
import BannerRecommends from './components/BannerRecommends';
import ComponentsList from './components/ComponentsList';
import DesignFramework from './components/DesignFramework';
import { ConfigProvider } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
const useStyle = () => {
const { token } = useSiteToken();
return {
container: css`
// padding: 0 116px;
// background: url(https://gw.alipayobjects.com/zos/bmw-prod/5741382d-cc22-4ede-b962-aea287a1d1a1/l4nq43o8_w2646_h1580.png);
// background-size: 20% 10%;
`,
};
};
const locales = {
cn: {
assetsTitle: '组件丰富,选用自如',
assetsDesc: '大量实用组件满足你的需求,灵活定制与拓展',
designTitle: '设计语言与研发框架',
designDesc: '配套生态,让你快速搭建网站应用',
},
en: {
assetsTitle: 'Rich components',
assetsDesc: 'Practical components to meet your needs, flexible customization and expansion',
designTitle: 'Design and framework',
designDesc: 'Supporting ecology, allowing you to quickly build website applications',
},
};
const Homepage: FC = () => {
const [locale, lang] = useLocale(locales);
const { id: localeId } = useDumiLocale();
const localeStr = localeId === 'zh-CN' ? 'cn' : 'en';
const [siteData, loading] = useSiteData();
const style = useStyle();
useLayoutEffect(() => {
if (lang === 'cn') {
dayjs.locale('zh-cn');
} else {
dayjs.locale('en');
}
}, []);
return (
<ConfigProvider theme={{ algorithm: undefined }}>
<section>
<Banner>
<BannerRecommends extras={siteData?.extras?.[localeStr]} icons={siteData?.icons} />
</Banner>
<div css={style.container}>
<Theme />
<Group
background="#fff"
collapse
title={locale.assetsTitle}
description={locale.assetsDesc}
id="design"
>
<ComponentsList />
</Group>
<Group
title={locale.designTitle}
description={locale.designDesc}
background="#F5F8FF"
decoration={
<>
{/* Image Left Top */}
<img
style={{ position: 'absolute', left: 0, top: -50, height: 160 }}
src="https://gw.alipayobjects.com/zos/bmw-prod/ba37a413-28e6-4be4-b1c5-01be1a0ebb1c.svg"
/>
</>
}
>
<DesignFramework />
</Group>
</div>
</section>
</ConfigProvider>
);
};
export default Homepage;

View File

@ -0,0 +1 @@
export { default } from '../theme-editor/index';

View File

@ -0,0 +1,36 @@
import { ThemeEditor } from 'antd-token-previewer';
import { useContext } from 'react';
import ThemeContext from '../../theme/slots/ThemeContext';
import useLocale from '../../hooks/useLocale';
import { ConfigProvider } from 'antd';
const locales = {
cn: {
title: '主题编辑器',
},
en: {
title: 'Theme Editor',
},
};
const CustomTheme = () => {
const [locale] = useLocale(locales);
const { setTheme, theme } = useContext(ThemeContext);
return (
<div>
<ConfigProvider theme={{ algorithm: undefined }}>
<ThemeEditor
theme={{ name: 'test', key: 'test', config: theme }}
simple
style={{ height: 'calc(100vh - 64px)' }}
onThemeChange={newTheme => {
setTheme(newTheme.config);
}}
/>
</ConfigProvider>
</div>
);
};
export default CustomTheme;

60
.dumi/rehypeAntd.ts Normal file
View File

@ -0,0 +1,60 @@
import assert from 'assert';
import { type HastRoot, type UnifiedTransformer, unistUtilVisit } from 'dumi';
/**
* plugin for modify hast tree when docs compiling
*/
function rehypeAntd(): UnifiedTransformer<HastRoot> {
return (tree, vFile) => {
unistUtilVisit.visit(tree, 'element', (node) => {
if (node.tagName === 'DumiDemoGrid') {
// replace DumiDemoGrid to DemoWrapper, to implement demo toolbar
node.tagName = 'DemoWrapper';
} else if (node.tagName === 'ResourceCards') {
const propNames = ['title', 'cover', 'description', 'src', 'official'];
const contentNode = node.children[0];
assert(
contentNode.type === 'text',
`ResourceCards content must be plain text!\nat ${
(vFile.data.frontmatter as any).filename
}`,
);
// clear children
node.children = [];
// generate JSX props
(node as any).JSXAttributes = [
{
type: 'JSXAttribute',
name: 'resources',
value: JSON.stringify(
contentNode.value
.trim()
.split('\n')
.reduce<any>((acc, cur) => {
// match text from ` - 桌面组件 Sketch 模板包`
const [, isProp, val] = cur.match(/(\s+)?-\s(.+)/)!;
if (!isProp) {
// create items when match title
acc.push({ [propNames[0]]: val });
} else {
// add props when match others
const prev = acc[acc.length - 1];
prev[propNames[Object.keys(prev).length]] = val;
}
return acc;
}, []),
),
},
];
}
});
};
}
export default rehypeAntd;

4
.dumi/theme/antd.js Normal file
View File

@ -0,0 +1,4 @@
// Need import for the additional core style
// exports.styleCore = require('../components/style/reset.css');
module.exports = require('../../components');

View File

@ -0,0 +1,8 @@
import React, { type FC } from 'react';
const APITable: FC = () => {
// TODO: implement api table, depend on the new markdown data structure passed
return <>API Table</>;
};
export default APITable;

View File

@ -0,0 +1,8 @@
import { Alert, AlertProps } from 'antd';
import React, { FC } from 'react';
const MdAlert: FC<AlertProps> = ({ style, ...props }) => {
return <Alert {...props} style={{ margin: '24px 0', ...style }} />;
};
export default MdAlert;

View File

@ -0,0 +1,4 @@
// @ts-ignore
import ColorPaletteTool from '../../common/Color/ColorPaletteTool';
export default ColorPaletteTool;

View File

@ -0,0 +1,4 @@
// @ts-ignore
import ColorPaletteToolDark from '../../common/Color/ColorPaletteToolDark';
export default ColorPaletteToolDark;

View File

@ -0,0 +1,4 @@
// @ts-ignore
import ColorPalettes from '../../common/Color/ColorPalettes';
export default ColorPalettes;

View File

@ -0,0 +1,54 @@
export type Component = {
title: string;
subtitle?: string;
cover: string;
link: string;
tag?: string;
};
const proComponentsList: Component[] = [
{
cover: 'https://gw.alipayobjects.com/zos/antfincdn/4n5H%24UX%24j/bianzu%2525204.svg',
link: 'https://procomponents.ant.design/components/layout',
subtitle: '高级布局',
title: 'ProLayout',
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
},
{
cover: 'https://gw.alipayobjects.com/zos/antfincdn/mStei5BFC/bianzu%2525207.svg',
link: 'https://procomponents.ant.design/components/form',
subtitle: '高级表单',
title: 'ProForm',
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
},
{
cover: 'https://gw.alipayobjects.com/zos/antfincdn/AwU0Cv%26Ju/bianzu%2525208.svg',
link: 'https://procomponents.ant.design/components/table',
subtitle: '高级表格',
title: 'ProTable',
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
},
{
cover: 'https://gw.alipayobjects.com/zos/antfincdn/H0%26LSYYfh/bianzu%2525209.svg',
link: 'https://procomponents.ant.design/components/descriptions',
subtitle: '高级定义列表',
title: 'ProDescriptions',
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
},
{
cover: 'https://gw.alipayobjects.com/zos/antfincdn/uZUmLtne5/bianzu%2525209.svg',
link: 'https://procomponents.ant.design/components/list',
subtitle: '高级列表',
title: 'ProList',
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
},
{
cover: 'https://gw.alipayobjects.com/zos/antfincdn/N3eU432oA/bianzu%2525209.svg',
link: 'https://procomponents.ant.design/components/editable-table',
subtitle: '可编辑表格',
title: 'EditableProTable',
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
},
];
export default proComponentsList;

View File

@ -0,0 +1,195 @@
import React, { useState, memo, useMemo } from 'react';
import { Link, useRouteMeta, useIntl, useSidebarData, Helmet } from 'dumi';
import { css } from '@emotion/react';
import debounce from 'lodash/debounce';
import { Input, Divider, Row, Col, Card, Typography, Tag, Space } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import proComponentsList from './ProComponentsList';
import type { Component } from './ProComponentsList';
import useSiteToken from '../../../hooks/useSiteToken';
const useStyle = () => {
const { token } = useSiteToken();
return {
componentsOverview: css`
padding: 0;
`,
componentsOverviewGroupTitle: css`
font-size: 24px;
margin-bottom: 24px !important;
`,
componentsOverviewTitle: css`
overflow: hidden;
color: ${token.colorTextHeading};
text-overflow: ellipsis;
`,
componentsOverviewImg: css`
display: flex;
align-items: center;
justify-content: center;
height: 152px;
background-color: ${token.colorBgElevated};
`,
componentsOverviewCard: css`
cursor: pointer;
transition: all 0.5s;
&:hover {
box-shadow: 0 6px 16px -8px #00000014, 0 9px 28px #0000000d, 0 12px 48px 16px #00000008;
}
`,
componentsOverviewSearch: css`
&${token.antCls}-input-affix-wrapper {
width: 100%;
padding: 0;
font-size: 20px;
border: 0;
box-shadow: none;
input {
color: rgba(0, 0, 0, 0.85);
font-size: 20px;
}
.anticon {
color: #bbb;
}
}
`,
};
};
const onClickCard = (pathname: string) => {
if (window.gtag) {
window.gtag('event', '点击', {
event_category: '组件总览卡片',
event_label: pathname,
});
}
};
const reportSearch = debounce<(value: string) => void>(value => {
if (window.gtag) {
window.gtag('event', '搜索', {
event_category: '组件总览卡片',
event_label: value,
});
}
}, 2000);
const { Title } = Typography;
const Overview: React.FC = () => {
const style = useStyle();
const meta = useRouteMeta();
const data = useSidebarData();
const { locale, formatMessage } = useIntl();
const documentTitle = `${meta.frontmatter.title} - Ant Design`;
const [search, setSearch] = useState<string>('');
const sectionRef = React.useRef<HTMLElement>(null);
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = event => {
if (event.keyCode === 13 && search.trim().length) {
sectionRef.current?.querySelector<HTMLElement>('.components-overview-card')?.click();
}
};
const groups = useMemo<{ title: string; children: Component[] }[]>(() => {
return data
.filter(item => item.title)
.map<{ title: string; children: Component[] }>(item => {
return {
title: item.title!,
children: item.children.map(child => ({
title: child.frontmatter.title,
subtitle: child.frontmatter.subtitle,
cover: child.frontmatter.cover,
link: child.link,
})),
};
})
.concat([
{
title: locale === 'zh-CN' ? '重型组件' : 'Others',
children: proComponentsList,
},
]);
}, [data, locale]);
return (
<section className="markdown" ref={sectionRef}>
<Divider />
<Input
value={search}
placeholder={formatMessage({ id: 'app.components.overview.search' })}
css={style.componentsOverviewSearch}
onChange={e => {
setSearch(e.target.value);
reportSearch(e.target.value);
}}
onKeyDown={onKeyDown}
autoFocus // eslint-disable-line jsx-a11y/no-autofocus
suffix={<SearchOutlined />}
/>
<Divider />
{groups
.filter(i => i.title)
.map(group => {
const components = group?.children?.filter(
component =>
!search.trim() ||
component.title.toLowerCase().includes(search.trim().toLowerCase()) ||
(component?.subtitle || '').toLowerCase().includes(search.trim().toLowerCase()),
);
return components?.length ? (
<div key={group.title} css={style.componentsOverview}>
<Title level={2} css={style.componentsOverviewGroupTitle}>
<Space align="center">
<span style={{ fontSize: 24 }}>{group.title}</span>
<Tag style={{ display: 'block' }}>{components.length}</Tag>
</Space>
</Title>
<Row gutter={[24, 24]}>
{components.map(component => {
const url = `${component.link}/`;
/** Link 不能跳转到外链 */
const ComponentLink = !url.startsWith('http') ? Link : 'a';
return (
<Col xs={24} sm={12} lg={8} xl={6} key={component.title}>
<ComponentLink to={url} href={url} onClick={() => onClickCard(url)}>
<Card
bodyStyle={{
backgroundRepeat: 'no-repeat',
backgroundPosition: 'bottom right',
backgroundImage: `url(${component?.tag || ''})`,
}}
size="small"
css={style.componentsOverviewCard}
title={
<div css={style.componentsOverviewTitle}>
{component.title} {component.subtitle}
</div>
}
>
<div css={style.componentsOverviewImg}>
<img src={component.cover} alt={component.title} />
</div>
</Card>
</ComponentLink>
</Col>
);
})}
</Row>
</div>
) : null;
})}
</section>
);
};
export default memo(Overview);

View File

@ -0,0 +1,63 @@
import React, { useContext, useLayoutEffect, useState } from 'react';
import { DumiDemoGrid, FormattedMessage } from 'dumi';
import { Tooltip } from 'antd';
import { BugFilled, BugOutlined, CodeFilled, CodeOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import DemoContext from '../../slots/DemoContext';
const DemoWrapper: typeof DumiDemoGrid = ({ items }) => {
const { showDebug, setShowDebug } = useContext(DemoContext);
const [expandAll, setExpandAll] = useState(false);
const expandTriggerClass = classNames('code-box-expand-trigger', {
'code-box-expand-trigger-active': expandAll,
});
const handleVisibleToggle = () => {
setShowDebug?.(!showDebug);
};
const handleExpandToggle = () => {
setExpandAll(!expandAll);
};
const visibleDemos = showDebug ? items : items.filter((item) => !item.previewerProps.debug);
const filteredItems = visibleDemos.map((item) => ({
...item,
previewerProps: { ...item.previewerProps, expand: expandAll },
}));
return (
<div className="demo-wrapper">
<span className="all-code-box-controls">
<Tooltip
title={
<FormattedMessage id={`app.component.examples.${expandAll ? 'collapse' : 'expand'}`} />
}
>
{expandAll ? (
<CodeFilled className={expandTriggerClass} onClick={handleExpandToggle} />
) : (
<CodeOutlined className={expandTriggerClass} onClick={handleExpandToggle} />
)}
</Tooltip>
<Tooltip
title={
<FormattedMessage id={`app.component.examples.${showDebug ? 'hide' : 'visible'}`} />
}
>
{showDebug ? (
<BugFilled className={expandTriggerClass} onClick={handleVisibleToggle} />
) : (
<BugOutlined className={expandTriggerClass} onClick={handleVisibleToggle} />
)}
</Tooltip>
</span>
{/* FIXME: find a new way instead of `key` to trigger re-render */}
<DumiDemoGrid items={filteredItems} key={expandAll + '' + showDebug} />
</div>
);
};
export default DemoWrapper;

View File

@ -1,6 +1,6 @@
import * as React from 'react';
import { message } from 'antd';
import { injectIntl } from 'react-intl';
import { useIntl } from 'dumi';
import CopyableIcon from './CopyableIcon';
import type { ThemeType } from './index';
import type { CategoriesKeys } from './fields';
@ -10,11 +10,11 @@ interface CategoryProps {
icons: string[];
theme: ThemeType;
newIcons: string[];
intl: any;
}
const Category: React.FC<CategoryProps> = props => {
const { icons, title, newIcons, theme, intl } = props;
const { icons, title, newIcons, theme } = props;
const intl = useIntl();
const [justCopied, setJustCopied] = React.useState<string | null>(null);
const copyId = React.useRef<NodeJS.Timeout | null>(null);
const onCopied = React.useCallback((type: string, text: string) => {
@ -38,7 +38,7 @@ const Category: React.FC<CategoryProps> = props => {
);
return (
<div>
<h3>{intl.messages[`app.docs.components.icon.category.${title}`]}</h3>
<h3>{intl.formatMessage({ id: `app.docs.components.icon.category.${title}` })}</h3>
<ul className="anticons-list">
{icons.map(name => (
<CopyableIcon
@ -55,4 +55,4 @@ const Category: React.FC<CategoryProps> = props => {
);
};
export default injectIntl(Category);
export default Category;

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Upload, Tooltip, Popover, Modal, Progress, message, Spin, Result } from 'antd';
import CopyToClipboard from 'react-copy-to-clipboard';
import { injectIntl } from 'react-intl';
import { useIntl } from 'dumi';
import * as AntdIcons from '@ant-design/icons';
const allIcons: { [key: string]: any } = AntdIcons;
@ -17,10 +17,6 @@ declare global {
}
}
interface PicSearcherProps {
intl: any;
}
interface PicSearcherState {
loading: boolean;
modalOpen: boolean;
@ -36,8 +32,8 @@ interface iconObject {
score: number;
}
const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
const { messages } = intl;
const PicSearcher: React.FC = () => {
const intl = useIntl();
const [state, setState] = useState<PicSearcherState>({
loading: false,
modalOpen: false,
@ -63,7 +59,7 @@ const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
}
};
// eslint-disable-next-line class-methods-use-this
const toImage = (url: string) =>
const toImage = (url: string): Promise<HTMLImageElement> =>
new Promise(resolve => {
const img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
@ -139,13 +135,13 @@ const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
return (
<div className="icon-pic-searcher">
<Popover
content={messages[`app.docs.components.icon.pic-searcher.intro`]}
content={intl.formatMessage({ id: `app.docs.components.icon.pic-searcher.intro` })}
open={state.popoverVisible}
>
<AntdIcons.CameraOutlined className="icon-pic-btn" onClick={toggleModal} />
</Popover>
<Modal
title={messages[`app.docs.components.icon.pic-searcher.title`]}
title={intl.formatMessage({ id: `app.docs.components.icon.pic-searcher.title` })}
open={state.modalOpen}
onCancel={toggleModal}
footer={null}
@ -153,7 +149,7 @@ const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
{state.modelLoaded || (
<Spin
spinning={!state.modelLoaded}
tip={messages['app.docs.components.icon.pic-searcher.modelloading']}
tip={intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.modelloading' })}
>
<div style={{ height: 100 }} />
</Spin>
@ -170,21 +166,21 @@ const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
<AntdIcons.InboxOutlined />
</p>
<p className="ant-upload-text">
{messages['app.docs.components.icon.pic-searcher.upload-text']}
{intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-text' })}
</p>
<p className="ant-upload-hint">
{messages['app.docs.components.icon.pic-searcher.upload-hint']}
{intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-hint' })}
</p>
</Dragger>
)}
<Spin
spinning={state.loading}
tip={messages['app.docs.components.icon.pic-searcher.matching']}
tip={intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.matching' })}
>
<div className="icon-pic-search-result">
{state.icons.length > 0 && (
<div className="result-tip">
{messages['app.docs.components.icon.pic-searcher.result-tip']}
{intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.result-tip' })}
</div>
)}
<table>
@ -192,9 +188,11 @@ const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
<thead>
<tr>
<th className="col-icon">
{messages['app.docs.components.icon.pic-searcher.th-icon']}
{intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-icon' })}
</th>
<th>
{intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-score' })}
</th>
<th>{messages['app.docs.components.icon.pic-searcher.th-score']}</th>
</tr>
</thead>
)}
@ -226,7 +224,9 @@ const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
<Result
status="500"
title="503"
subTitle={messages['app.docs.components.icon.pic-searcher.server-error']}
subTitle={intl.formatMessage({
id: 'app.docs.components.icon.pic-searcher.server-error',
})}
/>
)}
</div>
@ -236,4 +236,4 @@ const PicSearcher: React.FC<PicSearcherProps> = ({ intl }) => {
);
};
export default injectIntl(PicSearcher);
export default PicSearcher;

View File

@ -200,8 +200,6 @@ const logo = [
'Yahoo',
'Reddit',
'Sketch',
'WhatsApp',
'Dingtalk',
];
const datum = [...direction, ...suggestion, ...editor, ...data, ...logo];

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import Icon, * as AntdIcons from '@ant-design/icons';
import { Radio, Input, Empty } from 'antd';
import type { RadioChangeEvent } from 'antd/es/radio/interface';
import { injectIntl } from 'react-intl';
import { useIntl } from 'dumi';
import debounce from 'lodash/debounce';
import Category from './Category';
import IconPicSearcher from './IconPicSearcher';
@ -18,18 +18,14 @@ export enum ThemeType {
const allIcons: { [key: string]: any } = AntdIcons;
interface IconDisplayProps {
intl: any;
}
interface IconDisplayState {
interface IconSearchState {
theme: ThemeType;
searchKey: string;
}
const IconDisplay: React.FC<IconDisplayProps> = ({ intl }) => {
const { messages } = intl;
const [displayState, setDisplayState] = React.useState<IconDisplayState>({
const IconSearch: React.FC = () => {
const intl = useIntl();
const [displayState, setDisplayState] = React.useState<IconSearchState>({
theme: ThemeType.Outlined,
searchKey: '',
});
@ -51,8 +47,8 @@ const IconDisplay: React.FC<IconDisplayProps> = ({ intl }) => {
const { searchKey = '', theme } = displayState;
const categoriesResult = Object.keys(categories)
.map((key: CategoriesKeys) => {
let iconList = categories[key];
.map(key => {
let iconList = categories[key as CategoriesKeys];
if (searchKey) {
const matchKey = searchKey
// eslint-disable-next-line prefer-regex-literals
@ -83,7 +79,7 @@ const IconDisplay: React.FC<IconDisplayProps> = ({ intl }) => {
return categoriesResult.length === 0 ? <Empty style={{ margin: '2em 0' }} /> : categoriesResult;
}, [displayState.searchKey, displayState.theme]);
return (
<>
<div className="markdown">
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Radio.Group
value={displayState.theme}
@ -92,17 +88,20 @@ const IconDisplay: React.FC<IconDisplayProps> = ({ intl }) => {
buttonStyle="solid"
>
<Radio.Button value={ThemeType.Outlined}>
<Icon component={OutlinedIcon} /> {messages['app.docs.components.icon.outlined']}
<Icon component={OutlinedIcon} />{' '}
{intl.formatMessage({ id: 'app.docs.components.icon.outlined' })}
</Radio.Button>
<Radio.Button value={ThemeType.Filled}>
<Icon component={FilledIcon} /> {messages['app.docs.components.icon.filled']}
<Icon component={FilledIcon} />{' '}
{intl.formatMessage({ id: 'app.docs.components.icon.filled' })}
</Radio.Button>
<Radio.Button value={ThemeType.TwoTone}>
<Icon component={TwoToneIcon} /> {messages['app.docs.components.icon.two-tone']}
<Icon component={TwoToneIcon} />{' '}
{intl.formatMessage({ id: 'app.docs.components.icon.two-tone' })}
</Radio.Button>
</Radio.Group>
<Input.Search
placeholder={messages['app.docs.components.icon.search.placeholder']}
placeholder={intl.formatMessage({ id: 'app.docs.components.icon.search.placeholder' })}
style={{ margin: '0 10px', flex: 1 }}
allowClear
onChange={e => handleSearchIcon(e.currentTarget.value)}
@ -112,8 +111,8 @@ const IconDisplay: React.FC<IconDisplayProps> = ({ intl }) => {
/>
</div>
{renderCategories}
</>
</div>
);
};
export default injectIntl(IconDisplay);
export default IconSearch;

View File

@ -1,18 +1,11 @@
import * as React from 'react';
import type { CustomIconComponentProps } from '@ant-design/icons/es/components/Icon';
interface CustomIconComponentProps {
width: string | number;
height: string | number;
fill: string;
viewBox?: string;
className?: string;
style?: React.CSSProperties;
spin?: boolean;
rotate?: number;
['aria-hidden']?: React.AriaAttributes['aria-hidden'];
}
type CustomIconComponent = React.ComponentType<
CustomIconComponentProps | React.SVGProps<SVGSVGElement>
>;
export const FilledIcon: React.FC<CustomIconComponentProps> = props => {
export const FilledIcon: CustomIconComponent = props => {
const path =
'M864 64H160C107 64 64 107 64 160v' +
'704c0 53 43 96 96 96h704c53 0 96-43 96-96V16' +
@ -24,7 +17,7 @@ export const FilledIcon: React.FC<CustomIconComponentProps> = props => {
);
};
export const OutlinedIcon: React.FC<CustomIconComponentProps> = props => {
export const OutlinedIcon: CustomIconComponent = props => {
const path =
'M864 64H160C107 64 64 107 64 160v7' +
'04c0 53 43 96 96 96h704c53 0 96-43 96-96V160c' +
@ -38,7 +31,7 @@ export const OutlinedIcon: React.FC<CustomIconComponentProps> = props => {
);
};
export const TwoToneIcon: React.FC<CustomIconComponentProps> = props => {
export const TwoToneIcon: CustomIconComponent = props => {
const path =
'M16 512c0 273.932 222.066 496 496 49' +
'6s496-222.068 496-496S785.932 16 512 16 16 238.' +

View File

@ -0,0 +1,163 @@
import React from 'react';
import classNames from 'classnames';
import { Modal, Carousel } from 'antd';
function isGood(className) {
return /\bgood\b/i.test(className);
}
function isBad(className) {
return /\bbad\b/i.test(className);
}
function isInline(className) {
return /\binline\b/i.test(className);
}
function PreviewImageBox({
cover,
coverMeta,
imgs,
style,
previewVisible,
comparable,
onClick,
onCancel,
}) {
const onlyOneImg = comparable || imgs.length === 1;
const imageWrapperClassName = classNames('preview-image-wrapper', {
good: coverMeta.isGood,
bad: coverMeta.isBad,
});
return (
<div className="preview-image-box" style={style}>
<div onClick={onClick} className={imageWrapperClassName}>
<img className={coverMeta.className} src={coverMeta.src} alt={coverMeta.alt} />
</div>
<div className="preview-image-title">{coverMeta.alt}</div>
<div
className="preview-image-description"
dangerouslySetInnerHTML={{ __html: coverMeta.description }}
/>
<Modal
className="image-modal"
width={960}
visible={previewVisible}
title={null}
footer={null}
onCancel={onCancel}
>
<Carousel
className={`${onlyOneImg ? 'image-modal-single' : ''}`}
draggable={!onlyOneImg}
adaptiveHeight
>
{comparable ? cover : imgs}
</Carousel>
<div className="preview-image-title">{coverMeta.alt}</div>
</Modal>
</div>
);
}
function isGoodBadImg(imgMeta) {
return imgMeta.isGood || imgMeta.isBad;
}
function isCompareImg(imgMeta) {
return isGoodBadImg(imgMeta) || imgMeta.inline;
}
export default class ImagePreview extends React.Component {
constructor(props) {
super(props);
this.state = {
previewVisible: {},
};
}
handleClick = index => {
this.setState({
previewVisible: {
[index]: true,
},
});
};
handleCancel = () => {
this.setState({
previewVisible: {},
});
};
render() {
const { imgs } = this.props;
const imgsMeta = imgs.map(img => {
const { alt, description, src } = img;
const imgClassName = img.class;
return {
className: imgClassName,
alt,
description,
src,
isGood: isGood(imgClassName),
isBad: isBad(imgClassName),
inline: isInline(imgClassName),
};
});
const imagesList = imgsMeta.map((meta, index) => {
const metaCopy = { ...meta };
delete metaCopy.description;
delete metaCopy.isGood;
delete metaCopy.isBad;
return (
<div key={index}>
<div className="image-modal-container">
<img {...metaCopy} alt={meta.alt} />
</div>
</div>
);
});
const comparable =
(imgs.length === 2 && imgsMeta.every(isCompareImg)) ||
(imgs.length >= 2 && imgsMeta.every(isGoodBadImg));
const style = comparable ? { width: `${(100 / imgs.length).toFixed(3)}%` } : null;
const hasCarousel = imgs.length > 1 && !comparable;
const previewClassName = classNames({
'preview-image-boxes': true,
clearfix: true,
'preview-image-boxes-compare': comparable,
'preview-image-boxes-with-carousel': hasCarousel,
});
return (
<div className={previewClassName}>
{imagesList.map((_, index) => {
if (!comparable && index !== 0) {
return null;
}
return (
<PreviewImageBox
key={index}
style={style}
comparable={comparable}
previewVisible={!!this.state.previewVisible[index]}
cover={imagesList[index]}
coverMeta={imgsMeta[index]}
imgs={imagesList}
onClick={() => {
this.handleClick(index);
}}
onCancel={this.handleCancel}
/>
);
})}
</div>
);
}
}

View File

@ -0,0 +1,4 @@
// @ts-ignore
import Palette from '../../common/Color/Palette';
export default Palette;

View File

@ -0,0 +1,101 @@
import React, { useEffect, useState, type FC } from 'react';
// @ts-ignore
import JsonML from 'jsonml.js/lib/utils';
// @ts-ignore
import toReactComponent from 'jsonml-to-react-element';
// @ts-ignore
import Prism from 'prismjs';
import { useLocation } from 'dumi';
import { useIntl, type IPreviewerProps } from 'dumi';
import { ping } from '../../utils';
import sylvanas from 'sylvanas';
let pingDeferrer: PromiseLike<boolean>;
function useShowRiddleButton() {
const [showRiddleButton, setShowRiddleButton] = useState(false);
useEffect(() => {
pingDeferrer ??= new Promise<boolean>(resolve => {
ping(status => {
if (status !== 'timeout' && status !== 'error') {
return resolve(true);
}
return resolve(false);
});
});
pingDeferrer.then(setShowRiddleButton);
}, []);
return showRiddleButton;
}
/**
* HOC for convert dumi previewer props to bisheng previewer props
*/
export default function fromDumiProps<P extends object>(
WrappedComponent: React.ComponentType<P>,
): FC<IPreviewerProps> {
const hoc = function DumiPropsAntdPreviewer(props: IPreviewerProps) {
const showRiddleButton = useShowRiddleButton();
const location = useLocation();
const { asset, children, demoUrl, expand, description = '', ...meta } = props;
const intl = useIntl();
const entryCode = asset.dependencies['index.tsx'].value;
const transformedProps = {
meta: {
id: asset.id,
title: '',
filename: meta.filePath,
...meta,
},
content: description,
preview: () => children,
utils: {
toReactComponent(jsonML: any) {
return toReactComponent(jsonML, [
[
function (node: any) {
return JsonML.isElement(node) && JsonML.getTagName(node) === 'pre';
},
function (node: any, index: any) {
// @ts-ignore
// ref: https://github.com/benjycui/bisheng/blob/master/packages/bisheng/src/bisheng-plugin-highlight/lib/browser.js#L7
var attr = JsonML.getAttributes(node);
return React.createElement(
'pre',
{
key: index,
className: `language-${attr.lang}`,
},
React.createElement('code', {
dangerouslySetInnerHTML: { __html: attr.highlighted },
}),
);
},
],
]);
},
},
intl: { locale: intl.locale },
showRiddleButton,
highlightedCodes: {
jsx: Prism.highlight(meta.jsx, Prism.languages.javascript, 'jsx'),
tsx: Prism.highlight(entryCode, Prism.languages.javascript, 'tsx'),
},
style: meta.style,
location,
src: demoUrl,
expand,
// FIXME: confirm is there has any case?
highlightedStyle: '',
// FIXME: dumi support usePrefersColor
theme: 'light',
} as P;
return <WrappedComponent {...transformedProps} />;
};
return hoc;
}

View File

@ -7,13 +7,15 @@ import LZString from 'lz-string';
import React from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import ReactDOM from 'react-dom';
import { FormattedMessage, injectIntl } from 'react-intl';
import BrowserFrame from '../../BrowserFrame';
import EditButton from '../EditButton';
import CodePenIcon from './CodePenIcon';
import CodePreview from './CodePreview';
import CodeSandboxIcon from './CodeSandboxIcon';
import RiddleIcon from './RiddleIcon';
import { FormattedMessage } from 'dumi';
import BrowserFrame from '../../common/BrowserFrame';
import EditButton from '../../common/EditButton';
import CodePenIcon from '../../common/CodePenIcon';
import CodePreview from '../../common/CodePreview';
import CodeSandboxIcon from '../../common/CodeSandboxIcon';
import RiddleIcon from '../../common/RiddleIcon';
import ExternalLinkIcon from '../../common/ExternalLinkIcon';
import fromDumiProps from './fromDumiProps';
const { ErrorBoundary } = Alert;
@ -121,7 +123,7 @@ class Demo extends React.Component {
handleIframeReady = () => {
const { theme, setIframeTheme } = this.props;
if (this.iframeRef.current) {
setIframeTheme(this.iframeRef.current, theme);
// setIframeTheme(this.iframeRef.current, theme);
}
};
@ -137,7 +139,6 @@ class Demo extends React.Component {
style,
highlightedStyle,
expand,
utils,
intl: { locale },
theme,
showRiddleButton,
@ -166,16 +167,11 @@ class Demo extends React.Component {
});
const localizedTitle = meta.title[locale] || meta.title;
const localizeIntro = content[locale] || content;
const introChildren = utils.toReactComponent(['div'].concat(localizeIntro));
const introChildren = <div dangerouslySetInnerHTML={{ __html: localizeIntro }}></div>;
const highlightClass = classNames('highlight-wrapper', {
'highlight-wrapper-expand': codeExpand,
});
const prefillStyle = `@import '~antd/dist/antd.css';\n\n${style || ''}`.replace(
new RegExp(`#${meta.id}\\s*`, 'g'),
'',
);
const html = `<!DOCTYPE html>
<html lang="en">
<head>
@ -246,14 +242,12 @@ class Demo extends React.Component {
/export default/,
'const ComponentDemo =',
)}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
css: prefillStyle,
editors: '001',
css: '',
// eslint-disable-next-line no-undef
css_external: `https://unpkg.com/antd@${antdReproduceVersion}/dist/antd.css`,
js_external: [
'react@18/umd/react.development.js',
'react-dom@18/umd/react-dom.development.js',
'moment/min/moment-with-locales.js',
// eslint-disable-next-line no-undef
`antd@${antdReproduceVersion}/dist/antd-with-locales.js`,
`@ant-design/icons/dist/index.umd.js`,
@ -273,7 +267,7 @@ class Demo extends React.Component {
/export default/,
'const ComponentDemo =',
)}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
css: prefillStyle,
css: '',
json: JSON.stringify(
{
name: 'antd-demo',
@ -297,7 +291,6 @@ class Demo extends React.Component {
const demoJsContent = `
${importReactContent}
import 'antd/dist/antd.css';
import './index.css';
${parsedSourceCode}
`.trim();
@ -464,6 +457,12 @@ createRoot(document.getElementById('container')).render(<Demo />);
})}
</Tooltip>
</CopyToClipboard>
<Tooltip title={<FormattedMessage id="app.demo.separate" />}>
<a className="code-box-code-action" target="_blank" rel="noreferer" href={src}>
<ExternalLinkIcon className="code-box-separate" />
</a>
</Tooltip>
<Tooltip
title={<FormattedMessage id={`app.demo.code.${codeExpand ? 'hide' : 'show'}`} />}
>
@ -521,4 +520,4 @@ createRoot(document.getElementById('container')).render(<Demo />);
}
}
export default injectIntl(Demo);
export default fromDumiProps(Demo);

View File

@ -0,0 +1,154 @@
/* eslint-disable react/no-array-index-key */
import * as React from 'react';
import dayjs from 'dayjs';
import { FormattedMessage, useIntl } from 'dumi';
import { Tabs, Skeleton, Avatar, Divider, Empty } from 'antd';
import { useSiteData } from '../../../pages/index/components/util';
import type { Article, Authors } from '../../../pages/index/components/util';
import { css } from '@emotion/react';
import useSiteToken from '../../../hooks/useSiteToken';
const useStyle = () => {
const { token } = useSiteToken();
const { antCls } = token;
return {
articles: css`
h4 {
margin: 40px 0 24px;
font-weight: 500;
font-size: 20px;
}
${antCls}-skeleton {
h3 {
margin: 0;
}
ul li {
display: block;
margin-left: 0;
}
}
${antCls}-tabs-nav::before {
display: none;
}
table {
width: 100%;
table-layout: fixed;
td {
width: 50%;
vertical-align: top;
}
}
`,
articleList: css`
li {
margin: 1em 0;
padding: 0;
font-size: 14px;
list-style: none;
}
${antCls}-avatar > img {
max-width: unset;
}
`,
};
};
interface ArticleListProps {
name: React.ReactNode;
data: Article[];
authors: Authors;
}
const ArticleList: React.FC<ArticleListProps> = ({ name, data = [], authors = [] }) => {
const { articleList } = useStyle();
return (
<td>
<h4>{name}</h4>
<ul css={articleList}>
{data.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
data.map((article, index) => {
const author = authors.find(auth => auth.name === article.author);
return (
<li key={index}>
<a href={author?.href} target="_blank" rel="noreferrer">
<Avatar size="small" src={author?.avatar} />
</a>
<Divider type="vertical" />
<a href={article.href} target="_blank" rel="noreferrer">
{article.title}
</a>
</li>
);
})
)}
</ul>
</td>
);
};
export default () => {
const { locale } = useIntl();
const isZhCN = locale === 'zh-CN';
const [{ articles = { cn: [], en: [] }, authors = [] }, loading] = useSiteData();
const styles = useStyle();
// ========================== Data ==========================
const mergedData = React.useMemo(() => {
const yearData: Record<number | string, Record<string, Article[]>> = {};
articles[isZhCN ? 'cn' : 'en']?.forEach(article => {
const year = dayjs(article.date).year();
yearData[year] = yearData[year] || {};
yearData[year][article.type] = [...(yearData[year][article.type] || []), article];
});
return yearData;
}, [articles]);
// ========================= Render =========================
let content: React.ReactNode;
if (loading) {
content = <Skeleton active />;
} else {
const yearList = Object.keys(mergedData).sort((a, b) => Number(b) - Number(a));
content = yearList.length ? (
<Tabs>
{yearList.map(year => (
<Tabs.TabPane tab={`${year}${isZhCN ? ' 年' : ''}`} key={year}>
<table>
<tbody>
<tr>
<ArticleList
name={<FormattedMessage id="app.docs.resource.design" />}
data={mergedData[year].design}
authors={authors}
/>
<ArticleList
name={<FormattedMessage id="app.docs.resource.develop" />}
data={mergedData[year].develop}
authors={authors}
/>
</tr>
</tbody>
</table>
</Tabs.TabPane>
))}
</Tabs>
) : null;
}
return (
<div id="articles" css={styles.articles}>
{content}
</div>
);
};

View File

@ -0,0 +1,118 @@
import React, { type FC } from 'react';
import { Col, Row } from 'antd';
import { css } from '@emotion/react';
import useSiteToken from '../../../hooks/useSiteToken';
const useStyle = () => {
const { token } = useSiteToken();
const { boxShadowSecondary } = token;
return {
card: css`
position: relative;
display: flex;
flex-direction: column;
height: 100%;
color: inherit;
list-style: none;
border: 1px solid #e6e6e6;
border-radius: 2px;
cursor: pointer;
transition: box-shadow 0.3s;
&:hover {
box-shadow: ${boxShadowSecondary};
}
`,
image: css`
width: calc(100% + 2px);
max-width: none;
height: 184px;
margin: -1px -1px 0;
object-fit: cover;
`,
badge: css`
position: absolute;
top: 8px;
right: 8px;
padding: 4px 8px;
color: #fff;
font-size: 12px;
line-height: 1;
background: rgba(0, 0, 0, 0.65);
border-radius: 1px;
box-shadow: 0 0 2px rgba(255, 255, 255, 0.2);
`,
title: css`
margin: 16px 20px 8px;
color: #0d1a26;
font-size: 20px;
line-height: 28px;
`,
description: css`
margin: 0 20px 20px;
color: #697b8c;
font-size: 14px;
line-height: 22px;
`,
};
};
export type Resource = {
title: string;
description: string;
cover: string;
src: string;
official?: boolean;
};
export type ResourceCardProps = {
resource: Resource;
};
const ResourceCard: FC<ResourceCardProps> = ({ resource }) => {
const styles = useStyle();
const { title: titleStr, description, cover, src, official } = resource;
let coverColor: string | null = null;
let title: string = titleStr;
const titleMatch = titleStr.match(/(.*)(#[\dA-Fa-f]{6})/);
if (titleMatch) {
title = titleMatch[1].trim();
// eslint-disable-next-line prefer-destructuring
coverColor = titleMatch[2];
}
return (
<Col xs={24} sm={12} md={8} lg={6} style={{ padding: 12 }}>
<a css={styles.card} target="_blank" href={src}>
<img
css={styles.image}
src={cover}
alt={title}
style={coverColor ? { backgroundColor: coverColor } : {}}
/>
{official && <div css={styles.badge}>Official</div>}
<p css={styles.title}>{title}</p>
<p css={styles.description}>{description}</p>
</a>
</Col>
);
};
export type ResourceCardsProps = {
resources: Resource[];
};
const ResourceCards: FC<ResourceCardsProps> = ({ resources }) => {
return (
<Row style={{ margin: '-12px -12px 0 -12px' }}>
{resources.map(item => (
<ResourceCard resource={item} key={item.title} />
))}
</Row>
);
};
export default ResourceCards;

View File

@ -0,0 +1,5 @@
import React from 'react';
const BrowserFrame = ({ children }) => <div className="browser-mockup with-url">{children}</div>;
export default BrowserFrame;

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from 'dumi';
import ColorPicker from './ColorPicker';
import ColorPatterns from './ColorPatterns';

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from 'dumi';
import { Row, Col } from 'antd';
import ColorPicker from './ColorPicker';
import ColorPatterns from './ColorPatterns';

View File

@ -1,27 +1,52 @@
.make-palette(@color, @index: 1) when (@index <= 10) {
.palette-@{color}-@{index} {
@background: '@{color}-@{index}';
import { Global, css } from '@emotion/react';
import useSiteToken from '../../../hooks/useSiteToken';
background: @@background;
}
.make-palette(@color, (@index + 1)); // next iteration
const gray = {
1: '#fff',
2: '#fafafa',
3: '#f5f5f5',
4: '#f0f0f0',
5: '#d9d9d9',
6: '#bfbfbf',
7: '#8c8c8c',
8: '#595959',
9: '#434343',
10: '#262626',
11: '#1f1f1f',
12: '#141414',
13: '#000',
};
const ColorStyle = () => {
const { token } = useSiteToken();
const makePalette = (color: string, index: number = 1): string => {
if (index <= 10) {
return `
.palette-${color}-${index} {
background: ${(token as any)[`${color}-${index}`]};
}
${makePalette(color, index + 1)}
`;
}
return '';
};
@gray-1: #fff;
@gray-2: #fafafa;
@gray-3: #f5f5f5;
@gray-4: #f0f0f0;
@gray-5: #d9d9d9;
@gray-6: #bfbfbf;
@gray-7: #8c8c8c;
@gray-8: #595959;
@gray-9: #434343;
@gray-10: #262626;
@gray-11: #1f1f1f;
@gray-12: #141414;
@gray-13: #000;
@border-color: rgba(229, 231, 235, 100);
const makeGrayPalette = (index: number = 1): string => {
if (index <= 13) {
return `
.palette-gray-${index} {
background: ${(gray as any)[index]};
}
${makeGrayPalette(index + 1)}
`;
}
return '';
};
return (
<Global
styles={css`
.color-palettes {
margin: 0 1%;
@ -31,11 +56,11 @@
background-color: #141414;
.color-title {
color: fade(@white, 85);
color: rgba(255, 255, 255, 0.85);
}
.color-description {
color: fade(@white, 45);
color: rgba(255, 255, 255, 0.45);
}
.color-palette {
@ -87,7 +112,7 @@
position: relative;
top: -3px;
margin-left: 16px;
color: @error-color;
color: ${token.colorError};
font-size: 13px;
.ant-row-rtl & {
@ -103,31 +128,19 @@
}
.main-color {
.make-palette(blue);
.make-palette(purple);
.make-palette(cyan);
.make-palette(green);
.make-palette(magenta);
.make-palette(red);
.make-palette(volcano);
.make-palette(orange);
.make-palette(gold);
.make-palette(yellow);
.make-palette(lime);
.make-palette(geekblue);
.make-palette(gray);
.palette-gray-11 {
background: @gray-11;
}
.palette-gray-12 {
background: @gray-12;
}
.palette-gray-13 {
background: @gray-13;
}
${makePalette('blue')}
${makePalette('purple')}
${makePalette('cyan')}
${makePalette('green')}
${makePalette('magenta')}
${makePalette('red')}
${makePalette('volcano')}
${makePalette('orange')}
${makePalette('gold')}
${makePalette('yellow')}
${makePalette('lime')}
${makePalette('geekblue')}
${makeGrayPalette()}
text-align: left;
@ -197,6 +210,7 @@
.color-palette-horizontal {
width: 100%;
box-sizing: border-box;
&-dark {
height: 303px;
@ -208,11 +222,11 @@
}
.color-palette-pick {
color: fade(@white, 65);
color: rgba(255, 255, 255, 0.65);
text-align: left;
&-hex {
color: fade(@white, 65);
color: rgba(255, 255, 255, 0.65);
}
.ant-row-rtl & {
@ -226,6 +240,7 @@
display: flex;
&-item {
box-sizing: border-box;
position: relative;
flex: 1;
height: 86px;
@ -267,3 +282,9 @@
}
}
}
`}
/>
);
};
export default ColorStyle;

View File

@ -87,3 +87,7 @@ export default class Palette extends React.Component {
);
}
}
Palette.defaultProps = {
color: { name: 'gray', count: 13 },
}

View File

@ -0,0 +1,22 @@
import { useRouteMeta, Helmet } from 'dumi';
import React, { useMemo } from 'react';
const CommonHelmet = () => {
const meta = useRouteMeta();
const [title, description] = useMemo(() => {
const helmetTitle = `${meta.frontmatter.subtitle || ''} ${meta.frontmatter.title} - Ant Design`;
let helmetDescription = meta.frontmatter.description;
return [helmetTitle, helmetDescription];
}, [meta]);
return (
<Helmet>
<title>{title}</title>
<meta property="og:title" content={title} />
{description && <meta name="description" content={description} />}
</Helmet>
);
};
export default CommonHelmet;

View File

@ -0,0 +1,57 @@
import React from 'react';
import { Tooltip } from 'antd';
import { EditOutlined } from '@ant-design/icons';
import { css } from '@emotion/react';
import useSiteToken from '../../hooks/useSiteToken';
const branchUrl = 'https://github.com/ant-design/ant-design/edit/master/';
export interface EditButtonProps {
title: React.ReactNode;
filename?: string;
}
const useStyle = () => {
const { token } = useSiteToken();
const { colorIcon, colorText, iconCls } = token;
return {
editButton: css`
a& {
display: inline-block;
text-decoration: none;
margin-inline-start: 4px;
vertical-align: -2px;
${iconCls} {
display: block;
color: ${colorIcon};
font-size: 16px;
transition: all 0.3s;
&:hover {
color: ${colorText};
}
}
}
`,
};
};
export default function EditButton({ title, filename }: EditButtonProps) {
const styles = useStyle();
return (
<Tooltip title={title}>
<a
css={styles.editButton}
href={`${branchUrl}${filename}`}
target="_blank"
rel="noopener noreferrer"
>
<EditOutlined />
</a>
</Tooltip>
);
}

View File

@ -0,0 +1,13 @@
import React from 'react';
import Icon from '@ant-design/icons';
const SVGIcon = ({ color = 'currentColor' }) => (
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill={color}>
<path d="M853.333 469.333A42.667 42.667 0 0 0 810.667 512v256A42.667 42.667 0 0 1 768 810.667H256A42.667 42.667 0 0 1 213.333 768V256A42.667 42.667 0 0 1 256 213.333h256A42.667 42.667 0 0 0 512 128H256a128 128 0 0 0-128 128v512a128 128 0 0 0 128 128h512a128 128 0 0 0 128-128V512a42.667 42.667 0 0 0-42.667-42.667z" />
<path d="M682.667 213.333h67.413L481.707 481.28a42.667 42.667 0 0 0 0 60.587 42.667 42.667 0 0 0 60.586 0L810.667 273.92v67.413A42.667 42.667 0 0 0 853.333 384 42.667 42.667 0 0 0 896 341.333V170.667A42.667 42.667 0 0 0 853.333 128H682.667a42.667 42.667 0 0 0 0 85.333z" />
</svg>
);
const ExternalLinkIcon = props => <Icon component={SVGIcon} {...props} />;
export default ExternalLinkIcon;

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import type { HelmetProps } from 'react-helmet-async';
import { Helmet } from 'react-helmet-async';
import { Helmet } from 'dumi';
type HelmetProps = Helmet['props'];
export interface WrapHelmetProps extends HelmetProps {
children?: React.ReactNode;
}

View File

@ -0,0 +1,28 @@
import React, { type FC } from 'react';
import { Skeleton, Space, Spin } from 'antd';
import { useLocation } from 'dumi';
const Loading: FC = () => {
const { pathname } = useLocation();
if (
pathname.startsWith('/components') ||
pathname.startsWith('/docs') ||
pathname.startsWith('/changelog')
) {
return (
<Space direction="vertical" style={{ width: '100%', marginTop: 24 }} size={40}>
<Skeleton title={false} active paragraph={{ rows: 3 }} />
<Skeleton active paragraph={{ rows: 3 }} />
</Space>
);
}
return (
<Space style={{ width: '100%', margin: '120px 0', justifyContent: 'center' }} align="center">
<Spin size="large" />
</Space>
);
};
export default Loading;

View File

@ -0,0 +1,142 @@
import React, { ReactElement, useMemo } from 'react';
import { ClassNames, css } from '@emotion/react';
import useSiteToken from '../../hooks/useSiteToken';
import { Menu, MenuProps, Typography } from 'antd';
import useMenu from '../../hooks/useMenu';
import { MenuItemType } from 'antd/es/menu/hooks/useItems';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
const useStyle = () => {
const { token } = useSiteToken();
const { colorSplit, iconCls, fontSizeIcon } = token;
return {
prevNextNav: css`
width: calc(100% - 234px);
margin-inline-end: 170px;
margin-inline-start: 64px;
overflow: hidden;
font-size: 14px;
border-top: 1px solid ${colorSplit};
display: flex;
`,
pageNav: `
flex: 1;
height: 72px;
line-height: 72px;
text-decoration: none;
${iconCls} {
font-size: ${fontSizeIcon}px;
transition: all 0.3s;
}
.chinese {
margin-inline-start: 4px;
}
`,
prevNav: `
text-align: start;
.footer-nav-icon-after {
display: none;
}
.footer-nav-icon-before {
position: relative;
margin-inline-end: 1em;
vertical-align: middle;
line-height: 0;
right: 0;
transition: right 0.3s;
}
&:hover .footer-nav-icon-before {
right: 0.2em;
}
`,
nextNav: `
text-align: end;
.footer-nav-icon-before {
display: none;
}
.footer-nav-icon-after {
position: relative;
margin-inline-start: 1em;
margin-bottom: 1px;
vertical-align: middle;
line-height: 0;
left: 0;
transition: left 0.3s;
}
&:hover .footer-nav-icon-after {
left: 0.2em;
}
`,
};
};
const flattenMenu = (menuItems: MenuProps['items']): MenuProps['items'] | null => {
if (Array.isArray(menuItems)) {
return menuItems.reduce<Exclude<MenuProps['items'], undefined>>((acc, item) => {
if (!item) {
return acc;
}
if ('children' in item && item.children) {
return acc.concat(flattenMenu(item.children) ?? []);
}
return acc.concat(item);
}, []);
}
return null;
};
const PrevAndNext = () => {
const styles = useStyle();
const [menuItems, selectedKey] = useMenu({
before: <LeftOutlined className="footer-nav-icon-before" />,
after: <RightOutlined className="footer-nav-icon-after" />,
});
const [prev, next] = useMemo(() => {
const flatMenu = flattenMenu(menuItems);
if (!flatMenu) {
return [null, null];
}
let activeMenuItemIndex = -1;
flatMenu.forEach((menuItem, i) => {
if (menuItem && menuItem.key === selectedKey) {
activeMenuItemIndex = i;
}
});
const prev = flatMenu[activeMenuItemIndex - 1];
const next = flatMenu[activeMenuItemIndex + 1];
return [prev as MenuItemType, next as MenuItemType];
}, [menuItems, selectedKey]);
return (
<section css={styles.prevNextNav}>
<ClassNames>
{({ css: classCss, cx }) => (
<>
{prev &&
React.cloneElement(prev.label as ReactElement, {
className: cx(classCss(styles.pageNav), classCss(styles.prevNav)),
})}
{next &&
React.cloneElement(next.label as ReactElement, {
className: cx(classCss(styles.pageNav), classCss(styles.nextNav)),
})}
</>
)}
</ClassNames>
</section>
);
};
export default PrevAndNext;

View File

@ -0,0 +1,148 @@
import React, { type FC, useEffect, useMemo, useRef } from 'react';
import { useOutlet, useSearchParams, Helmet } from 'dumi';
import Header from 'dumi/theme/slots/Header';
import Footer from 'dumi/theme/slots/Footer';
import '../../static/style';
import useLocation from '../../../hooks/useLocation';
import SiteContext from '../../slots/SiteContext';
import ConfigProvider, { DirectionType } from 'antd/es/config-provider';
import classNames from 'classnames';
import useLocale from '../../../hooks/useLocale';
import zhCN from 'antd/es/locale/zh_CN';
import { createCache, StyleProvider } from '@ant-design/cssinjs';
import ResourceLayout from '../ResourceLayout';
import GlobalStyles from '../../common/GlobalStyles';
import SidebarLayout from '../SidebarLayout';
const styleCache = createCache();
if (typeof global !== 'undefined') {
(global as any).styleCache = styleCache;
}
const locales = {
cn: {
title: 'Ant Design - 一套企业级 UI 设计语言和 React 组件库',
description: '基于 Ant Design 设计体系的 React UI 组件库,用于研发企业级中后台产品。',
},
en: {
title: "Ant Design - The world's second most popular React UI framework",
description:
'An enterprise-class UI design language and React UI library with a set of high-quality React components, one of best React UI library for enterprises',
},
};
const RESPONSIVE_MOBILE = 768;
const DocLayout: FC = () => {
const outlet = useOutlet();
const location = useLocation();
const { pathname, search } = location;
const [searchParams, setSearchParams] = useSearchParams();
const [locale, lang] = useLocale(locales);
// TODO: place doc layout here, apply for all docs route paths
// migrate from: https://github.com/ant-design/ant-design/blob/eb9179464b9c4a93c856e1e70ddbdbaaf3f3371f/site/theme/template/Layout/index.tsx
const [isMobile, setIsMobile] = React.useState<boolean>(false);
const [direction, setDirection] = React.useState<DirectionType>('ltr');
const timerRef = useRef<NodeJS.Timeout | null>(null);
const updateMobileMode = () => {
setIsMobile(window.innerWidth < RESPONSIVE_MOBILE);
};
useEffect(() => {
const nprogressHiddenStyle = document.getElementById('nprogress-style');
if (nprogressHiddenStyle) {
timerRef.current = setTimeout(() => {
nprogressHiddenStyle.parentNode?.removeChild(nprogressHiddenStyle);
}, 0);
}
// Handle direction
const queryDirection = searchParams.get('direction');
setDirection(queryDirection === 'rtl' ? 'rtl' : 'ltr');
// Handle mobile mode
updateMobileMode();
window.addEventListener('resize', updateMobileMode);
return () => {
window.removeEventListener('resize', updateMobileMode);
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);
React.useEffect(() => {
if (typeof (window as any).ga !== 'undefined') {
(window as any).ga('send', 'pageview', pathname + search);
}
if (typeof (window as any)._hmt !== 'undefined') {
(window as any)._hmt.push(['_trackPageview', pathname + search]);
}
}, [location]);
const changeDirection = (direction: DirectionType): void => {
setDirection(direction);
if (direction === 'ltr') {
searchParams.delete('direction');
} else {
searchParams.set('direction', 'rtl');
}
setSearchParams(searchParams);
};
const content = useMemo(() => {
if (
['', '/'].some(path => path === pathname) ||
['/index'].some(path => pathname.startsWith(path))
) {
return (
<>
{outlet}
<Footer />
</>
);
} else if (pathname.startsWith('/docs/resource')) {
return <ResourceLayout>{outlet}</ResourceLayout>;
} else if (pathname.startsWith('/theme-editor')) {
return <>{outlet}</>;
}
return <SidebarLayout>{outlet}</SidebarLayout>;
}, [pathname, outlet]);
return (
<StyleProvider cache={styleCache}>
<SiteContext.Provider value={{ isMobile, direction }}>
<Helmet encodeSpecialCharacters={false}>
<html
lang={lang}
data-direction={direction}
className={classNames({ [`rtl`]: direction === 'rtl' })}
/>
<title>{locale.title}</title>
<link
sizes="144x144"
href="https://gw.alipayobjects.com/zos/antfincdn/UmVnt3t4T0/antd.png"
/>
<meta name="description" content={locale.description} />
<meta property="og:title" content={locale.title} />
<meta property="og:type" content="website" />
<meta
property="og:image"
content="https://gw.alipayobjects.com/zos/rmsportal/rlpTLlbMzTNYuZGGCVYM.png"
/>
</Helmet>
<ConfigProvider locale={lang === 'cn' ? zhCN : undefined} direction={direction}>
<GlobalStyles />
<Header changeDirection={changeDirection} />
{content}
</ConfigProvider>
</SiteContext.Provider>
</StyleProvider>
);
};
export default DocLayout;

View File

@ -0,0 +1,63 @@
import React, { type FC, useLayoutEffect } from 'react';
import { useOutlet } from 'dumi';
import { ConfigProvider, theme as antdTheme } from 'antd';
import { ThemeConfig } from 'antd/es/config-provider/context';
import ThemeContext, { ThemeContextProps } from '../slots/ThemeContext';
const ANT_DESIGN_SITE_THEME = 'antd-site-theme';
const GlobalLayout: FC = () => {
const outlet = useOutlet();
const [theme, setTheme] = React.useState<ThemeConfig>({});
const contextValue = React.useMemo<ThemeContextProps>(
() => ({
theme,
setTheme: newTheme => {
setTheme(newTheme);
localStorage.setItem(
ANT_DESIGN_SITE_THEME,
JSON.stringify(newTheme, (key, value) => {
if (key === 'algorithm') {
return value === antdTheme.darkAlgorithm ? 'dark' : value;
}
return value;
}),
);
},
}),
[theme],
);
useLayoutEffect(() => {
const localTheme = localStorage.getItem(ANT_DESIGN_SITE_THEME);
if (localTheme) {
try {
const themeConfig = JSON.parse(localTheme);
if (themeConfig.algorithm === 'dark') {
themeConfig.algorithm = antdTheme.darkAlgorithm;
}
setTheme(themeConfig);
} catch (e) {
console.error(e);
}
}
}, []);
return (
<ThemeContext.Provider value={contextValue}>
<ConfigProvider
theme={{
...theme,
// TODO: Site algorithm
// algorithm: undefined,
}}
>
{outlet}
</ConfigProvider>
</ThemeContext.Provider>
);
};
export default GlobalLayout;

View File

@ -3,11 +3,53 @@ import classNames from 'classnames';
import throttle from 'lodash/throttle';
import { Tabs } from 'antd';
import scrollTo from '../../../../components/_util/scrollTo';
import useSiteToken from '../../../hooks/useSiteToken';
import { css } from '@emotion/react';
import './AffixTabs.less';
const useStyle = () => {
const { token } = useSiteToken();
const { boxShadowSecondary, antCls } = token;
return {
affixTabs: css`
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 11;
padding: 0 40px;
background: #fff;
box-shadow: ${boxShadowSecondary};
transform: translateY(-100%);
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
${antCls}-tabs {
max-width: 1208px;
margin: 0 auto;
${antCls}-tabs-nav {
margin: 0;
&::before {
border-bottom-color: transparent;
}
${antCls}-tabs-tab {
padding: 21px 0;
}
}
}
`,
affixTabsFixed: css`
transform: translateY(0);
opacity: 1;
`,
};
};
const VIEW_BALANCE = 32;
const { TabPane } = Tabs;
export default () => {
const containerRef = React.useRef<HTMLDivElement>(null);
@ -15,6 +57,8 @@ export default () => {
const [loaded, setLoaded] = React.useState(false);
const [fixedId, setFixedId] = React.useState<string | null>(null);
const styles = useStyle();
function scrollToId(id: string) {
const targetNode = document.getElementById(id);
@ -70,22 +114,17 @@ export default () => {
}, []);
return (
<div
className={classNames('resource-affix-tabs', {
'resource-affix-tabs-fixed': fixedId,
})}
ref={containerRef}
>
<div css={[styles.affixTabs, fixedId && styles.affixTabsFixed]} ref={containerRef}>
<Tabs
activeKey={fixedId || undefined}
onChange={key => {
scrollToId(key);
}}
>
{idsRef.current.map(id => (
<TabPane key={id} tab={id.replace(/-/g, ' ')} />
))}
</Tabs>
items={idsRef.current.map(id => ({
key: id,
label: <span style={{ textTransform: 'capitalize' }}>{id.replace(/-/g, ' ')}</span>,
}))}
/>
</div>
);
};

View File

@ -0,0 +1,140 @@
import React, { FC, PropsWithChildren } from 'react';
import { useRouteMeta, FormattedMessage } from 'dumi';
import Footer from 'dumi/theme/slots/Footer';
import { Layout, Typography, ConfigProvider } from 'antd';
import { css } from '@emotion/react';
import AffixTabs from './AffixTabs';
import EditButton from '../../common/EditButton';
import useSiteToken from '../../../hooks/useSiteToken';
import CommonHelmet from '../../common/CommonHelmet';
export type ResourceLayoutProps = PropsWithChildren<{}>;
const useStyle = () => {
const { token } = useSiteToken();
const { antCls } = token;
const resourcePadding = 40;
const articleMaxWidth = 1208;
const resourcePaddingXS = 24;
return {
resourcePage: css`
footer {
margin-top: 176px;
.rc-footer-container {
max-width: ${articleMaxWidth}px;
margin: 0 auto;
padding-right: 0;
padding-left: 0;
}
}
`,
resourceContent: css`
padding: 0 ${resourcePadding}px;
max-width: ${articleMaxWidth}px;
margin: 0 auto;
box-sizing: content-box;
> .markdown {
> p {
margin-bottom: 56px;
}
h2 {
margin-top: 124px;
color: #314659;
font-weight: lighter;
font-size: 30px;
line-height: 38px;
&:first-child {
margin-top: 88px;
}
}
h3 {
margin-top: 56px;
font-weight: 400;
font-size: 24px;
line-height: 32px;
}
p {
color: #697b8c;
}
}
@media only screen and (max-width: 767.99px) {
& {
article {
padding: 0 ${resourcePaddingXS}px;
}
${antCls}-col {
padding-top: 16px !important;
padding-bottom: 16px !important;
}
}
}
`,
banner: css`
padding: 0 ${resourcePadding}px;
overflow: hidden;
background: url('https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*y_r7RogIG1wAAAAAAAAAAABkARQnAQ');
background-size: cover;
h1 {
box-sizing: content-box;
max-width: ${articleMaxWidth}px;
margin: 56px auto 16px;
}
section {
max-width: ${articleMaxWidth}px;
margin: 0 auto 56px;
font-weight: 200;
font-size: 16px;
line-height: 24px;
}
@media only screen and (max-width: 767.99px) {
& {
margin: 0 -${resourcePaddingXS}px;
padding: 0 ${resourcePaddingXS}px;
}
}
`,
};
};
const ResourceLayout: FC<ResourceLayoutProps> = ({ children }) => {
const styles = useStyle();
const meta = useRouteMeta();
return (
<ConfigProvider theme={{ token: { colorBgLayout: '#fff' } }}>
<Layout>
<CommonHelmet />
<div id="resources-page" css={styles.resourcePage}>
<AffixTabs />
<div css={styles.banner}>
<Typography.Title style={{ fontSize: 30 }}>
{meta.frontmatter.title}
<EditButton
title={<FormattedMessage id="app.content.edit-page" />}
filename={meta.frontmatter.filename}
/>
</Typography.Title>
<section>{meta.frontmatter.description}</section>
</div>
<div css={styles.resourceContent}>{children}</div>
<Footer />
</div>
</Layout>
</ConfigProvider>
);
};
export default ResourceLayout;

View File

@ -0,0 +1,16 @@
import React, { FC, PropsWithChildren } from 'react';
import Sidebar from '../../slots/Sidebar';
import Content from '../../slots/Content';
import CommonHelmet from '../../common/CommonHelmet';
const SidebarLayout: FC<PropsWithChildren<{}>> = ({ children }) => {
return (
<main style={{ display: 'flex', marginTop: 40 }}>
<CommonHelmet />
<Sidebar />
<Content>{children}</Content>
</main>
);
};
export default SidebarLayout;

View File

@ -0,0 +1,141 @@
{
"app.theme.switch.dynamic": "Dynamic Theme",
"app.theme.switch.default": "Default theme",
"app.theme.switch.dark": "Dark theme",
"app.theme.switch.compact": "Compact theme",
"app.header.search": "Search...",
"app.header.menu.documentation": "Docs",
"app.header.menu.development": "Development",
"app.header.menu.components": "Components",
"app.header.menu.spec": "Design",
"app.header.menu.resource": "Resources",
"app.header.menu.more": "More",
"app.header.menu.mobile": "Mobile",
"app.header.menu.pro.v4": "Ant Design Pro",
"app.header.menu.pro.components": "Ant Design Pro Components",
"app.header.menu.charts": "Ant Design Charts",
"app.header.menu.ecosystem": "Ecosystem",
"app.header.lang": "中文",
"app.content.edit-page": "Edit this page on GitHub!",
"app.content.edit-demo": "Edit this demo on GitHub!",
"app.content.contributors": "contributors",
"app.component.examples": "Examples",
"app.component.examples.expand": "Expand all code",
"app.component.examples.collapse": "Collapse all code",
"app.component.examples.visible": "Expand debug examples",
"app.component.examples.hide": "Collapse debug examples",
"app.component.examples.openDemoNotReact18": "Open Demo with React < 18",
"app.component.examples.openDemoWithReact18": "Open Demo with React 18",
"app.demo.debug": "Debug only, won't display at online",
"app.demo.copy": "Copy code",
"app.demo.copied": "Copied!",
"app.demo.code.show": "Show code",
"app.demo.code.hide": "Hide code",
"app.demo.codepen": "Open in CodePen",
"app.demo.codesandbox": "Open in CodeSandbox",
"app.demo.stackblitz": "Open in Stackblitz",
"app.demo.riddle": "Open in Riddle",
"app.demo.separate": "Open in a new window",
"app.home.introduce": "A design system for enterprise-level products. Create an efficient and enjoyable work experience.",
"app.home.pr-welcome": "💡 It is an alpha version and still in progress. Contribution from community is welcome!",
"app.home.recommend": "Recommended",
"app.home.popularize": "Popular",
"app.home.design-and-framework": "Design language and framework",
"app.home.design-values": "Design values",
"app.home.design-values-description": "This is Ant Design's internal standard for evaluating design quality. Based on the assumption that \"everyone is pursuing happiness at work\", we have added the two values of \"Meaningfulness\" and \"Growth\" on the basis of \"Certainty\" and \"Naturalness\" to guide each designer towards better judgment and decision-making.",
"app.home.certainty": "Certainty",
"app.home.meaningful": "Meaningfulness",
"app.home.growth": "Growth",
"app.home.natural": "Naturalness",
"app.home.design-guide": "Guidelines",
"app.home.components": "Components",
"app.home.detail": "More details",
"app.home.global-style": "Global style",
"app.home.design-patterns": "Design patterns",
"app.home.more": "Learn More",
"app.home.getting-started": "Getting Started",
"app.home.design-language": "Design Language",
"app.home.product-antv-slogan": "A new way to do data visualization",
"app.home.product-pro-slogan": "Out-of-the-box UI solution for enterprise applications",
"app.home.product-mobile-slogan": "Mobile UI components with Ant Design",
"app.home.product-hitu": "HiTu",
"app.home.product-hitu-slogan": "A new generation of graphical solutions",
"app.home.product-kitchen-slogan": "A Sketch plugin to enhance designers",
"app.home.product-icons-slogan": "A set of premium icons",
"app.home.view-more": "More",
"app.footer.repo": "GitHub Repository",
"app.footer.awesome": "Awesome Ant Design",
"app.footer.course": "Ant Design Practical Tutorial",
"app.footer.chinamirror": "China Mirror 🇨🇳",
"app.footer.primary-color-changing": "Changing primary color...",
"app.footer.primary-color-changed": "Changed primary color successfully!",
"app.footer.scaffold": "Scaffold",
"app.footer.kitchen": "Sketch Toolkit",
"app.footer.landing": "Landing Templates",
"app.footer.scaffolds": "Scaffold Market",
"app.footer.dev-tools": "Developer Tools",
"app.footer.umi": "React Application Framework",
"app.footer.dumi": "Component doc generator",
"app.footer.qiankun": "Micro-Frontends Framework",
"app.footer.hooks": "React Hooks Library",
"app.footer.resources": "Resources",
"app.footer.data-vis": "Data Visualization",
"app.footer.eggjs": "Enterprise Node Framework",
"app.footer.motion": "Motion Solution",
"app.footer.antd-library": "Axure library",
"app.footer.antux": "Sitemap Template",
"app.footer.community": "Community",
"app.footer.help": "Help",
"app.footer.change-log": "Change Log",
"app.footer.theme": "Theme Editor",
"app.footer.faq": "FAQ",
"app.footer.feedback": "Feedback",
"app.footer.stackoverflow": "StackOverflow",
"app.footer.segmentfault": "SegmentFault",
"app.footer.discussions": "Discussions",
"app.footer.bug-report": "Bug Report",
"app.footer.issues": "Issues",
"app.footer.version": "Version: ",
"app.footer.author": "Created by XTech",
"app.footer.work_with_us": "Work with Us",
"app.footer.more-product": "More Products",
"app.footer.company": "XTech",
"app.footer.ant-design": "UI Design Language",
"app.footer.yuque": "YuQue",
"app.footer.yuque.slogan": "Document Collaboration Platform",
"app.footer.antv.slogan": "Data Visualization",
"app.footer.egg.slogan": "Enterprise Node.js Framework",
"app.footer.yuque.repo": "Ant Design in YuQue",
"app.footer.zhihu": "Ant Design in Zhihu",
"app.footer.zhihu.xtech": "Experience Cloud Blog",
"app.footer.seeconf": "Experience Tech Conference",
"app.footer.xtech": "Ant Financial Experience Tech",
"app.footer.xtech.slogan": "Experience The Beauty",
"app.docs.color.pick-primary": "Pick your primary color",
"app.docs.color.pick-background": "Pick your background color",
"app.docs.components.icon.search.placeholder": "Search icons here, click icon to copy code",
"app.docs.components.icon.outlined": "Outlined",
"app.docs.components.icon.filled": "Filled",
"app.docs.components.icon.two-tone": "Two Tone",
"app.docs.components.icon.category.direction": "Directional Icons",
"app.docs.components.icon.category.suggestion": "Suggested Icons",
"app.docs.components.icon.category.editor": "Editor Icons",
"app.docs.components.icon.category.data": "Data Icons",
"app.docs.components.icon.category.other": "Application Icons",
"app.docs.components.icon.category.logo": "Brand and Logos",
"app.docs.components.icon.pic-searcher.intro": "AI Search by image is online, you are welcome to use it! 🎉",
"app.docs.components.icon.pic-searcher.title": "Search by image",
"app.docs.components.icon.pic-searcher.upload-text": "Click, drag, or paste file to this area to upload",
"app.docs.components.icon.pic-searcher.upload-hint": "We will find the best matching icon based on the image provided",
"app.docs.components.icon.pic-searcher.server-error": "Predict service is temporarily unavailable",
"app.docs.components.icon.pic-searcher.matching": "Matching...",
"app.docs.components.icon.pic-searcher.modelloading": "Model is loading...",
"app.docs.components.icon.pic-searcher.result-tip": "Matched the following icons for you:",
"app.docs.components.icon.pic-searcher.th-icon": "Icon",
"app.docs.components.icon.pic-searcher.th-score": "Probability",
"app.docs.resource.design": "Design",
"app.docs.resource.develop": "Develop",
"app.components.overview.search": "Search in components",
"app.implementation.community": "community",
"app.implementation.official": "official"
}

View File

@ -0,0 +1,140 @@
{
"app.theme.switch.dynamic": "动态主题",
"app.theme.switch.default": "默认主题",
"app.theme.switch.dark": "暗黑主题",
"app.theme.switch.compact": "紧凑主题",
"app.header.search": "全文本搜索...",
"app.header.menu.documentation": "文档",
"app.header.menu.development": "研发",
"app.header.menu.components": "组件",
"app.header.menu.spec": "设计",
"app.header.menu.resource": "资源",
"app.header.menu.more": "更多",
"app.header.menu.mobile": "移动版",
"app.header.menu.pro.v4": "Ant Design Pro",
"app.header.menu.pro.components": "Ant Design Pro Components",
"app.header.menu.charts": "Ant Design Charts",
"app.header.menu.ecosystem": "生态",
"app.header.lang": "English",
"app.content.edit-page": "在 GitHub 上编辑此页!",
"app.content.edit-demo": "在 GitHub 上编辑此示例!",
"app.content.contributors": "文档贡献者",
"app.component.examples": "代码演示",
"app.component.examples.expand": "展开全部代码",
"app.component.examples.collapse": "收起全部代码",
"app.component.examples.visible": "显示调试专用演示",
"app.component.examples.hide": "隐藏调试专用演示",
"app.component.examples.openDemoNotReact18": "使用 React 18 以下版本打开 Demo",
"app.component.examples.openDemoWithReact18": "使用 React 18 打开 Demo",
"app.demo.debug": "此演示仅供调试,线上不会展示",
"app.demo.copy": "复制代码",
"app.demo.copied": "复制成功",
"app.demo.code.show": "显示代码",
"app.demo.code.hide": "收起代码",
"app.demo.codepen": "在 CodePen 中打开",
"app.demo.codesandbox": "在 CodeSandbox 中打开",
"app.demo.stackblitz": "在 Stackblitz 中打开",
"app.demo.riddle": "在 Riddle 中打开",
"app.demo.separate": "在新窗口打开",
"app.home.introduce": "企业级产品设计体系,创造高效愉悦的工作体验",
"app.home.pr-welcome": "💡 当前为 alpha 版本,仍在开发中。欢迎社区一起共建,让 Ant Design 变得更好!",
"app.home.recommend": "精彩推荐",
"app.home.popularize": "推广",
"app.home.design-and-framework": "设计语言与研发框架",
"app.home.design-values": "设计价值观",
"app.home.design-values-description": "这是 Ant Design 评价设计好坏的内在标准。基于「每个人都追求快乐工作」这一假定,我们在「确定性」和「自然」的基础上,新增「意义感」和「生长性」两个价值观,指引每个设计者做更好地判断和决策。",
"app.home.certainty": "确定性",
"app.home.meaningful": "意义感",
"app.home.growth": "生长性",
"app.home.natural": "自然",
"app.home.design-guide": "设计指引",
"app.home.components": "组件库",
"app.home.detail": "查看详情",
"app.home.global-style": "全局样式",
"app.home.design-patterns": "设计模式",
"app.home.more": "更多内容",
"app.home.getting-started": "开始使用",
"app.home.design-language": "设计语言",
"app.home.product-antv-slogan": "全新一代数据可视化解决方案",
"app.home.product-pro-slogan": "开箱即用的中台前端/设计解决方案",
"app.home.product-mobile-slogan": "基于 Preact / React / React Native 的 UI 组件库",
"app.home.product-hitu": "海兔",
"app.home.product-hitu-slogan": "全新一代图形化解决方案",
"app.home.product-kitchen-slogan": "一款为设计者提升工作效率的 Sketch 工具集",
"app.home.product-icons-slogan": "一整套优质的图标集",
"app.home.view-more": "查看全部",
"app.footer.repo": "GitHub 仓库",
"app.footer.awesome": "Awesome Ant Design",
"app.footer.chinamirror": "国内镜像站点 🇨🇳",
"app.footer.primary-color-changing": "正在修改主题色...",
"app.footer.primary-color-changed": "修改主题色成功!",
"app.footer.kitchen": "Sketch 工具集",
"app.footer.landing": "首页模板集",
"app.footer.scaffold": "脚手架",
"app.footer.scaffolds": "脚手架市场",
"app.footer.dev-tools": "开发工具",
"app.footer.umi": "React 应用开发框架",
"app.footer.dumi": "组件/文档研发工具",
"app.footer.qiankun": "微前端框架",
"app.footer.hooks": "React Hooks 库",
"app.footer.resources": "相关资源",
"app.footer.data-vis": "数据可视化",
"app.footer.eggjs": "企业级 Node 开发框架",
"app.footer.motion": "设计动效",
"app.footer.antd-library": "Axure 部件库",
"app.footer.antux": "页面逻辑素材",
"app.footer.community": "社区",
"app.footer.help": "帮助",
"app.footer.change-log": "更新日志",
"app.footer.theme": "主题编辑器",
"app.footer.faq": "常见问题",
"app.footer.feedback": "反馈和建议",
"app.footer.stackoverflow": "StackOverflow",
"app.footer.segmentfault": "SegmentFault",
"app.footer.discussions": "讨论区",
"app.footer.bug-report": "报告 Bug",
"app.footer.issues": "讨论列表",
"app.footer.version": "文档版本:",
"app.footer.author": "蚂蚁集团体验技术部出品 @ XTech",
"app.footer.work_with_us": "加入我们",
"app.footer.more-product": "更多产品",
"app.footer.company": "XTech",
"app.footer.ant-design": "蚂蚁 UI 体系",
"app.footer.yuque": "语雀",
"app.footer.yuque.slogan": "构建你的数字花园",
"app.footer.antv.slogan": "数据可视化解决方案",
"app.footer.egg.slogan": "企业级 Node.js 框架",
"app.footer.yuque.repo": "Ant Design 语雀专栏",
"app.footer.zhihu": "Ant Design 知乎专栏",
"app.footer.zhihu.xtech": "体验科技专栏",
"app.footer.seeconf": "蚂蚁体验科技大会",
"app.footer.xtech": "蚂蚁体验科技",
"app.footer.xtech.slogan": "让用户体验美好",
"app.docs.color.pick-primary": "选择你的主色",
"app.docs.color.pick-background": "选择你的背景色",
"app.docs.components.icon.search.placeholder": "在此搜索图标,点击图标可复制代码",
"app.docs.components.icon.outlined": "线框风格",
"app.docs.components.icon.filled": "实底风格",
"app.docs.components.icon.two-tone": "双色风格",
"app.docs.components.icon.category.direction": "方向性图标",
"app.docs.components.icon.category.suggestion": "提示建议性图标",
"app.docs.components.icon.category.editor": "编辑类图标",
"app.docs.components.icon.category.data": "数据类图标",
"app.docs.components.icon.category.other": "网站通用图标",
"app.docs.components.icon.category.logo": "品牌和标识",
"app.docs.components.icon.pic-searcher.intro": "AI 截图搜索上线了,快来体验吧!🎉",
"app.docs.components.icon.pic-searcher.title": "上传图片搜索图标",
"app.docs.components.icon.pic-searcher.upload-text": "点击/拖拽/粘贴上传图片",
"app.docs.components.icon.pic-searcher.upload-hint": "我们会通过上传的图片进行匹配,得到最相似的图标",
"app.docs.components.icon.pic-searcher.server-error": "识别服务暂不可用",
"app.docs.components.icon.pic-searcher.matching": "匹配中...",
"app.docs.components.icon.pic-searcher.modelloading": "神经网络模型加载中...",
"app.docs.components.icon.pic-searcher.result-tip": "为您匹配到以下图标:",
"app.docs.components.icon.pic-searcher.th-icon": "图标",
"app.docs.components.icon.pic-searcher.th-score": "匹配度",
"app.docs.resource.design": "设计",
"app.docs.resource.develop": "开发",
"app.components.overview.search": "搜索组件",
"app.implementation.community": "社区实现",
"app.implementation.official": "官方"
}

116
.dumi/theme/plugin.ts Normal file
View File

@ -0,0 +1,116 @@
import fs from 'fs';
import type { IApi, IRoute } from 'dumi';
import { extractStyle } from '@ant-design/cssinjs';
import ReactTechStack from 'dumi/dist/techStacks/react';
import sylvanas from 'sylvanas';
/**
* extends dumi internal tech stack, for customize previewer props
*/
class AntdReactTechStack extends ReactTechStack {
// eslint-disable-next-line class-methods-use-this
generatePreviewerProps(...[props, opts]: any) {
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 = sylvanas.parseText(code);
if (md) {
// extract description & css style from md file
const description = md.match(
new RegExp(`(?:^|\\n)## ${locale}([^]+?)(\\n## [a-z]|\\n\`\`\`|\\n<style>|$)`),
)?.[1];
const style = md.match(/\n(?:```css|<style>)\n([^]+?)\n(?:```|<\/style>)/)?.[1];
props.description ??= description?.trim();
props.style ??= style;
}
}
return props;
}
}
const resolve = (path: string): string => require.resolve(path);
const RoutesPlugin = (api: IApi) => {
const ssrCssFileName = `ssr-${Date.now()}.css`;
api.registerTechStack(() => new AntdReactTechStack());
api.modifyRoutes((routes) => {
// TODO: append extra routes, such as home, changelog, form-v3
const extraRoutesList: IRoute[] = [
{
id: 'changelog-cn',
path: 'changelog-cn',
absPath: '/changelog-cn',
parentId: 'DocLayout',
file: resolve('../../CHANGELOG.zh-CN.md'),
},
{
id: 'changelog',
path: 'changelog',
absPath: '/changelog',
parentId: 'DocLayout',
file: resolve('../../CHANGELOG.en-US.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(':'))
// FIXME: workaround to make emotion support react 18 pipeableStream
// ref: https://github.com/emotion-js/emotion/issues/2800#issuecomment-1221296308
.map((file) => {
let styles = '';
// extract all emotion style tags from body
file.content = file.content.replace(/<style data-emotion[\s\S\n]+?<\/style>/g, (s) => {
styles += s;
return '';
});
// insert emotion style tags to head
file.content = file.content.replace('</head>', `${styles}</head>`);
return file;
}),
);
// add ssr css file to html
api.modifyConfig((memo) => {
memo.styles ??= [];
memo.styles.push(`/${ssrCssFileName}`);
return memo;
});
// generate ssr css file
api.onBuildHtmlComplete(() => {
const styleText = extractStyle((global as any).styleCache);
const styleTextWithoutStyleTag = styleText
.replace(/<style\s[^>]*>/g, '')
.replace(/<\/style>/g, '');
fs.writeFileSync(`./_site/${ssrCssFileName}`, styleTextWithoutStyleTag, 'utf8');
});
};
export default RoutesPlugin;

View File

@ -0,0 +1,215 @@
import React, { ReactNode, type FC, useMemo, useState, useLayoutEffect, useContext } from 'react';
import { useIntl, useRouteMeta } from 'dumi';
import Footer from 'dumi/theme/slots/Footer';
import { Col, Typography, Avatar, Tooltip, Affix, Anchor } from 'antd';
import EditButton from '../../common/EditButton';
import { FormattedMessage } from 'dumi';
import useLocation from '../../../hooks/useLocation';
import ContributorsList from '@qixian.cs/github-contributors-list';
import useSiteToken from '../../../hooks/useSiteToken';
import { css } from '@emotion/react';
import PrevAndNext from '../../common/PrevAndNext';
import DemoContext, { DemoContextProps } from '../DemoContext';
import classNames from 'classnames';
import SiteContext from '../SiteContext';
const useStyle = () => {
const { token } = useSiteToken();
const { antCls } = token;
return {
contributorsList: css`
display: flex;
flex-wrap: wrap;
margin-top: 120px !important;
a,
${antCls}-avatar + ${antCls}-avatar {
margin-bottom: 8px;
margin-inline-end: 8px;
}
`,
toc: css`
${antCls}-anchor {
${antCls}-anchor-link-title {
font-size: 12px;
}
}
`,
tocWrapper: css`
position: absolute;
top: 8px;
right: 0;
width: 160px;
margin: 12px 0;
padding: 8px 8px 8px 4px;
backdrop-filter: blur(8px);
border-radius: ${token.borderRadius}px;
box-sizing: border-box;
.toc-debug {
color: ${token['purple-6']};
&:hover {
color: ${token['purple-5']};
}
}
> div {
box-sizing: border-box;
width: 100%;
max-height: calc(100vh - 40px);
margin: 0 auto;
overflow: auto;
padding-inline: 4px;
}
&.rtl {
right: auto;
left: 20px;
}
@media only screen and (max-width: ${token.screenLG}px) {
display: none;
}
`,
articleWrapper: css`
padding: 0 170px 32px 64px;
&.rtl {
padding: 0 64px 144px 170px;
}
@media only screen and (max-width: ${token.screenLG}px) {
&, &.rtl {
padding-right: 48px;
padding-left: 48px;
}
}
`,
};
};
type AnchorItem = {
id: string;
title: string;
children?: AnchorItem[];
};
const Content: FC<{ children: ReactNode }> = ({ children }) => {
const meta = useRouteMeta();
const { pathname } = useLocation();
const { formatMessage } = useIntl();
const styles = useStyle();
const { direction } = useContext(SiteContext);
const [showDebug, setShowDebug] = useState(false);
const debugDemos = useMemo(
() => meta.toc?.filter((item) => item._debug_demo).map((item) => item.id) || [],
[meta],
);
useLayoutEffect(() => {
setShowDebug(process.env.NODE_ENV === 'development');
}, []);
const contextValue = useMemo<DemoContextProps>(
() => ({ showDebug, setShowDebug }),
[showDebug, debugDemos],
);
const anchorItems = useMemo(() => {
return meta.toc.reduce<AnchorItem[]>((result, item) => {
if (item.depth === 2) {
result.push({ ...item });
} else if (item.depth === 3) {
const parent = result[result.length - 1];
if (parent) {
parent.children = parent.children || [];
parent.children.push({ ...item });
}
}
return result;
}, []);
}, [meta.toc]);
const isRTL = direction === 'rtl';
return (
<DemoContext.Provider value={contextValue}>
<Col xxl={20} xl={19} lg={18} md={18} sm={24} xs={24}>
<Affix>
<section css={styles.tocWrapper} className={classNames({ rtl: isRTL })}>
<Anchor css={styles.toc} affix={false} showInkInFixed>
{anchorItems.map((item) => (
<Anchor.Link href={`#${item.id}`} title={item.title} key={item.id}>
{item.children
?.filter((child) => showDebug || !debugDemos.includes(child.id))
.map((child) => (
<Anchor.Link
href={`#${child.id}`}
title={
<span
className={classNames(debugDemos.includes(child.id) && 'toc-debug')}
>
{child.title}
</span>
}
key={child.id}
/>
))}
</Anchor.Link>
))}
</Anchor>
</section>
</Affix>
<article css={styles.articleWrapper} className={classNames({ rtl: isRTL })}>
<Typography.Title style={{ fontSize: 30 }}>
{meta.frontmatter.title}
{meta.frontmatter.subtitle && (
<span style={{ marginLeft: 12 }}>{meta.frontmatter.subtitle}</span>
)}
{!pathname.startsWith('/components/overview') && (
<EditButton
title={<FormattedMessage id="app.content.edit-page" />}
filename={meta.frontmatter.filename}
/>
)}
</Typography.Title>
{children}
<ContributorsList
css={styles.contributorsList}
fileName={meta.frontmatter.filename ?? ''}
renderItem={(item, loading) =>
loading ? (
<Avatar style={{ opacity: 0.3 }} />
) : (
item && (
<Tooltip
title={`${formatMessage({ id: 'app.content.contributors' })}: ${item.username}`}
key={item.username}
>
<a
href={`https://github.com/${item.username}`}
target="_blank"
rel="noopener noreferrer"
>
<Avatar src={item.url}>{item.username}</Avatar>
</a>
</Tooltip>
)
)
}
repo="ant-design"
owner="ant-design"
/>
</article>
<PrevAndNext />
<Footer />
</Col>
</DemoContext.Provider>
);
};
export default Content;

View File

@ -0,0 +1,12 @@
import { createContext } from 'react';
export type DemoContextProps = {
showDebug?: boolean;
};
const DemoContext = createContext<{
showDebug?: boolean;
setShowDebug?: (showDebug: boolean) => void;
}>({});
export default DemoContext;

View File

@ -0,0 +1,194 @@
import * as React from 'react';
import { Modal, Button, Typography, Row, Col, Tour } from 'antd';
import { SmileOutlined } from '@ant-design/icons';
import { isLocalStorageNameSupported, ping } from '../../utils';
import useLocale from '../../../hooks/useLocale';
import useSiteToken from '../../../hooks/useSiteToken';
const locales = {
cn: {
title: '🎉🎉🎉 Ant Design 5.0 发布! 🎉🎉🎉',
ok: '知道了',
},
en: {
title: '🎉🎉🎉 Ant Design 5.0 is released! 🎉🎉🎉',
ok: 'Got it',
},
};
const V5_NOTIFICATION = 'antd@4.0.0-notification-sent';
const SHOULD_OPEN_ANT_DESIGN_MIRROR_MODAL = 'ANT_DESIGN_DO_NOT_OPEN_MIRROR_MODAL';
function disableAntdMirrorModal() {
window.localStorage.setItem(SHOULD_OPEN_ANT_DESIGN_MIRROR_MODAL, 'true');
}
function shouldOpenAntdMirrorModal() {
return !window.localStorage.getItem(SHOULD_OPEN_ANT_DESIGN_MIRROR_MODAL);
}
export default function InfoNewVersion() {
return null;
const [locale, lang] = useLocale(locales);
const [notify, setNotify] = React.useState<null | boolean>(null);
const { token } = useSiteToken();
function onClose() {
setNotify(false);
localStorage.setItem(V5_NOTIFICATION, 'true');
}
React.useEffect(() => {
if (isLocalStorageNameSupported()) {
// 大版本发布后全局弹窗提示
// 1. 点击『知道了』之后不再提示
// 2. 超过截止日期后不再提示
if (
localStorage.getItem(V5_NOTIFICATION) !== 'true' &&
Date.now() < new Date('2022/12/31').getTime()
) {
setNotify(true);
return;
}
}
setNotify(false);
}, []);
React.useEffect(() => {
const timeout = ping((status) => {
if (status !== 'timeout' && status !== 'error') {
if (
// process.env.NODE_ENV === 'production' &&
notify === false &&
window.location.host !== 'ant-design.antgroup.com' &&
shouldOpenAntdMirrorModal()
) {
Modal.confirm({
title: '提示',
content: '内网用户推荐访问国内镜像以获得极速体验~',
okText: '🚀 立刻前往',
cancelText: '不再弹出',
closable: true,
onOk() {
window.open('https://ant-design.antgroup.com', '_self');
disableAntdMirrorModal();
},
onCancel() {
disableAntdMirrorModal();
},
});
}
}
});
return clearTimeout(timeout);
}, [notify]);
return (
<>
<Tour
open={!!notify}
mask={false}
steps={[
{
title: locale.title,
target: () => document.querySelector('#versionSelector')!,
description: (
<Typography style={{ marginTop: token.marginXS }}>
{lang === 'cn' ? (
<>
<p>
{' '}
<Typography.Link href="/changelog-cn" onClick={onClose}>
</Typography.Link>{' '}
</p>
<p>
访 v4 {' '}
<Typography.Link href="https://4x.ant.design/" onClick={onClose}>
</Typography.Link>
</p>
</>
) : (
<>
<p>
Click{' '}
<Typography.Link href="/changelog" onClick={onClose}>
here
</Typography.Link>{' '}
to view full changelog.
</p>
<p>
If you want to check v4 documentation, please click{' '}
<Typography.Link href="https://4x.ant.design/" onClick={onClose}>
here
</Typography.Link>
.
</p>
</>
)}
</Typography>
),
},
]}
/>
{/* <Modal
open={!!notify}
title={locale.title}
closable={false}
footer={<Button onClick={onClose}>{locale.ok}</Button>}
>
<Row gutter={16}>
<Col flex="none">
<SmileOutlined style={{ fontSize: 72, color: token.colorSuccess }} />
</Col>
<Col flex="auto">
<Typography style={{ marginTop: token.marginXS }}>
{lang === 'cn' ? (
<>
<p>
{' '}
<Typography.Link href="/changelog-cn" onClick={onClose}>
</Typography.Link>{' '}
</p>
<p>
访 v4 {' '}
<Typography.Link href="https://4x.ant.design/" onClick={onClose}>
</Typography.Link>
</p>
</>
) : (
<>
<p>
Click{' '}
<Typography.Link href="/changelog" onClick={onClose}>
here
</Typography.Link>{' '}
to view full changelog.
</p>
<p>
If you want to check v4 documentation, please click{' '}
<Typography.Link href="https://4x.ant.design/" onClick={onClose}>
here
</Typography.Link>
.
</p>
</>
)}
</Typography>
</Col>
</Row>
</Modal> */}
</>
);
}

View File

@ -1,47 +1,91 @@
import React, { useMemo } from 'react';
import React from 'react';
import RcFooter from 'rc-footer';
import { Link } from 'bisheng/router';
import type { WrappedComponentProps } from 'react-intl';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Link, FormattedMessage } from 'dumi';
import type { FooterColumn } from 'rc-footer/lib/column';
import {
AntDesignOutlined,
MediumOutlined,
TwitterOutlined,
ZhihuOutlined,
UsergroupAddOutlined,
BgColorsOutlined,
BugOutlined,
GithubOutlined,
HistoryOutlined,
ProfileOutlined,
BugOutlined,
IssuesCloseOutlined,
MediumOutlined,
ProfileOutlined,
QuestionCircleOutlined,
BgColorsOutlined,
TwitterOutlined,
UsergroupAddOutlined,
ZhihuOutlined,
YuqueOutlined,
} from '@ant-design/icons';
import type { FooterColumn } from 'rc-footer/lib/column';
import { getLocalizedPathname } from '../utils';
import useLocation from '../../../hooks/useLocation';
import { css } from '@emotion/react';
import useLocale from '../../../hooks/useLocale';
import useSiteToken from '../../../hooks/useSiteToken';
import { TinyColor } from '@ctrl/tinycolor';
import getAlphaColor from 'antd/es/theme/util/getAlphaColor';
import AdditionalInfo from './AdditionalInfo';
const Footer: React.FC<WrappedComponentProps & { location: any }> = props => {
const { intl, location } = props;
const getColumns = useMemo<FooterColumn[]>(() => {
const isZhCN = intl.locale === 'zh-CN';
const getLinkHash = (path: string, hash: { zhCN: string; enUS: string }) => {
const pathName = getLocalizedPathname(path, isZhCN, location.query, hash);
const { pathname, query = {} } = pathName;
const pathnames = pathname.split('#');
if ('direction' in query) {
return `${pathnames[0]}?direction=rtl#${pathnames[1]}`;
}
return pathname;
};
const locales = {
cn: {
owner: '蚂蚁集团和 Ant Design 开源社区',
},
en: {
owner: 'Ant Group and Ant Design Community',
},
};
const getLink = (path: string) => {
const pathName = getLocalizedPathname(path, isZhCN, location.query);
const { pathname, query = {} } = pathName;
if ('direction' in query) {
return `${pathname}?direction=rtl}`;
const useStyle = () => {
const { token } = useSiteToken();
const background = new TinyColor(getAlphaColor('#f0f3fa', '#fff'))
.onBackground(token.colorBgContainer)
.toHexString();
return {
holder: css`
background: ${background};
`,
footer: css`
background: ${background};
color: ${token.colorTextSecondary};
box-shadow: inset 0 106px 36px -116px rgba(0, 0, 0, 0.14);
* {
box-sizing: border-box;
}
return pathname;
};
h2,
a {
color: ${token.colorText};
}
.rc-footer-column {
margin-bottom: 0;
}
.rc-footer-container {
max-width: 1208px;
margin-inline: auto;
padding-inline: ${token.marginXXL}px;
}
.rc-footer-bottom {
font-size: ${token.fontSize}px;
box-shadow: inset 0 106px 36px -116px rgba(0, 0, 0, 0.14);
}
`,
};
};
const Footer = () => {
const location = useLocation();
const [locale, lang] = useLocale(locales);
const style = useStyle();
const { getLink } = location;
const getColumns = React.useMemo<FooterColumn[]>(() => {
const isZhCN = lang === 'cn';
const col1 = {
title: <FormattedMessage id="app.footer.resources" />,
@ -136,6 +180,12 @@ const Footer: React.FC<WrappedComponentProps & { location: any }> = props => {
url: 'http://twitter.com/antdesignui',
openExternal: true,
},
{
icon: <YuqueOutlined style={{ color: '#00b96b' }} />,
title: <FormattedMessage id="app.footer.yuque.repo" />,
url: 'https://yuque.com/ant-design/ant-design',
openExternal: true,
},
{
icon: <ZhihuOutlined style={{ color: '#0084ff' }} />,
title: <FormattedMessage id="app.footer.zhihu" />,
@ -167,9 +217,9 @@ const Footer: React.FC<WrappedComponentProps & { location: any }> = props => {
col2.items.push({
icon: <UsergroupAddOutlined />,
title: <FormattedMessage id="app.footer.work_with_us" />,
url: getLinkHash('/docs/resources', {
zhCN: '加入我们',
enUS: 'JoinUs',
url: getLink('/docs/resources', {
cn: '加入我们',
en: 'JoinUs',
}),
LinkComponent: Link,
} as unknown as typeof col2['items'][number]);
@ -300,31 +350,28 @@ const Footer: React.FC<WrappedComponentProps & { location: any }> = props => {
{
icon: <BgColorsOutlined />,
title: <FormattedMessage id="app.footer.theme" />,
url: getLinkHash('/components/config-provider/', {
zhCN: 'components-config-provider-demo-theme',
enUS: 'components-config-provider-demo-theme',
}),
url: getLink('/theme-editor'),
LinkComponent: Link,
},
],
};
return [col1, col2, col3, col4];
}, [intl.locale, location.query]);
}, [lang, location.search]);
return (
<RcFooter
columns={getColumns}
bottom={
<>
Made with <span style={{ color: '#fff' }}></span> by
{/* eslint-disable-next-line react/jsx-curly-brace-presence */}{' '}
<a target="_blank" rel="noopener noreferrer" href="https://xtech.antfin.com">
<FormattedMessage id="app.footer.company" />
</a>
</>
}
/>
<>
<RcFooter
columns={getColumns}
css={style.footer}
bottom={
<>
Made with <span style={{ color: '#fff' }}></span> by {locale.owner}
</>
}
/>
<AdditionalInfo />
</>
);
};
export default injectIntl(Footer);
export default Footer;

View File

@ -0,0 +1,77 @@
import * as React from 'react';
import classNames from 'classnames';
// @ts-ignore
import GitHubButton from 'react-github-button';
import useSiteToken from '../../../hooks/useSiteToken';
import { css } from '@emotion/react';
const useStyle = () => {
const { token } = useSiteToken();
const { antCls, colorPrimary } = token;
return {
githubBtn: css`
display: flex;
flex-flow: nowrap;
height: auto;
.gh-btn {
height: auto;
padding: 1px 4px;
background: transparent;
border: 0;
.gh-ico {
width: 20px;
height: 20px;
margin: 0;
}
.gh-text {
display: none;
}
}
.gh-count {
height: auto;
padding: 4px 8px;
color: #000;
font-weight: normal;
background: #fff;
&:hover {
color: ${colorPrimary};
}
}
${antCls}-row-rtl & {
.gh-count {
display: none !important;
}
}
`,
responsiveMode: css`
.gh-count {
display: none !important;
}
`,
};
};
export interface GithubProps {
responsive: null | 'narrow' | 'crowded';
}
export default ({ responsive }: GithubProps) => {
const style = useStyle();
return (
<GitHubButton
css={[style.githubBtn, responsive && style.responsiveMode]}
type="stargazers"
namespace="ant-design"
repo="ant-design"
/>
);
};

View File

@ -0,0 +1,76 @@
import * as React from 'react';
import { Link, useLocation } from 'dumi';
import * as utils from '../../utils';
import { css } from '@emotion/react';
import useSiteToken from '../../../hooks/useSiteToken';
const useStyle = () => {
const { token } = useSiteToken();
const { antCls, headerHeight, colorTextHeading, fontFamily, mobileMaxWidth } = token;
return {
logo: css`
height: ${headerHeight}px;
padding-left: 40px;
overflow: hidden;
color: ${colorTextHeading};
font-weight: bold;
font-size: 18px;
font-family: PuHuiTi, ${fontFamily}, sans-serif;
line-height: ${headerHeight}px;
letter-spacing: -0.18px;
white-space: nowrap;
text-decoration: none;
display: inline-flex;
align-items: center;
&:hover {
color: ${colorTextHeading};
}
${antCls}-row-rtl & {
float: right;
padding-right: 40px;
padding-left: 0;
}
img {
height: 32px;
margin-right: 12px;
vertical-align: middle;
${antCls}-row-rtl & {
margin-right: 0;
margin-left: 16px;
}
}
@media only screen and (max-width: ${mobileMaxWidth}px) {
padding-right: 0;
padding-left: 0;
}
`,
};
};
export interface LogoProps {
isZhCN: boolean;
location: any;
}
const Logo = ({ isZhCN }: LogoProps) => {
const { search } = useLocation();
const { logo } = useStyle();
return (
<h1>
<Link to={utils.getLocalizedPathname('/', isZhCN, search)} css={logo}>
<img alt="logo" src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg" />
<span style={{ lineHeight: '32px' }}>Ant Design</span>
</Link>
</h1>
);
};
export default Logo;

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import type { MenuProps } from 'antd';
import { Dropdown, Button } from 'antd';
import { FormattedMessage } from 'react-intl';
import { Dropdown, Menu, Button } from 'antd';
import { FormattedMessage } from 'dumi';
import { DownOutlined } from '@ant-design/icons';
import type { SharedProps } from './interface';
@ -11,12 +11,7 @@ export function getEcosystemGroup(): Exclude<MenuProps['items'], undefined> {
return [
{
label: (
<a
href="https://charts.ant.design"
className="header-link"
target="_blank"
rel="noopener noreferrer"
>
<a href="https://charts.ant.design" target="_blank" rel="noopener noreferrer">
<FormattedMessage id="app.header.menu.charts" />
</a>
),
@ -24,12 +19,7 @@ export function getEcosystemGroup(): Exclude<MenuProps['items'], undefined> {
},
{
label: (
<a
href="http://pro.ant.design"
className="header-link"
target="_blank"
rel="noopener noreferrer"
>
<a href="http://pro.ant.design" target="_blank" rel="noopener noreferrer">
<FormattedMessage id="app.header.menu.pro.v4" />
</a>
),
@ -37,12 +27,7 @@ export function getEcosystemGroup(): Exclude<MenuProps['items'], undefined> {
},
{
label: (
<a
href="http://procomponents.ant.design"
className="header-link"
target="_blank"
rel="noopener noreferrer"
>
<a href="http://procomponents.ant.design" target="_blank" rel="noopener noreferrer">
<FormattedMessage id="app.header.menu.pro.components" />
</a>
),
@ -50,12 +35,7 @@ export function getEcosystemGroup(): Exclude<MenuProps['items'], undefined> {
},
{
label: (
<a
href="http://ng.ant.design"
className="header-link"
target="_blank"
rel="noopener noreferrer"
>
<a href="http://ng.ant.design" target="_blank" rel="noopener noreferrer">
Ant Design of Angular
<span style={smallStyle}>
(<FormattedMessage id="app.implementation.community" />)
@ -66,12 +46,7 @@ export function getEcosystemGroup(): Exclude<MenuProps['items'], undefined> {
},
{
label: (
<a
href="http://antdv.com"
className="header-link"
target="_blank"
rel="noopener noreferrer"
>
<a href="http://antdv.com" target="_blank" rel="noopener noreferrer">
Ant Design of Vue
<span style={smallStyle}>
(<FormattedMessage id="app.implementation.community" />)

View File

@ -1,21 +1,87 @@
import * as React from 'react';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { Link } from 'bisheng/router';
import { Link, useLocation, FormattedMessage } from 'dumi';
import type { MenuProps } from 'antd';
import { MenuOutlined } from '@ant-design/icons';
import { Menu } from 'antd';
import { getEcosystemGroup } from './More';
import * as utils from '../../utils';
import type { SharedProps } from './interface';
import useSiteToken from '../../../hooks/useSiteToken';
import { css } from '@emotion/react';
import './Navigation.less';
const useStyle = () => {
const { token } = useSiteToken();
const { antCls, iconCls, fontFamily, headerHeight, menuItemBorder, colorPrimary } = token;
return {
nav: css`
height: 100%;
font-size: 14px;
font-family: Avenir, ${fontFamily}, sans-serif;
border: 0;
&${antCls}-menu-horizontal {
border-bottom: none;
& > ${antCls}-menu-item, & > ${antCls}-menu-submenu {
min-width: (40px + 12px * 2);
height: ${headerHeight}px;
padding-right: 12px;
padding-left: 12px;
line-height: ${headerHeight}px;
&::after {
top: 0;
right: 12px;
bottom: auto;
left: 12px;
border-width: ${menuItemBorder}px;
}
}
& ${antCls}-menu-submenu-title ${iconCls} {
margin: 0;
}
& > ${antCls}-menu-item-selected {
a {
color: ${colorPrimary};
}
}
}
& > ${antCls}-menu-item, & > ${antCls}-menu-submenu {
text-align: center;
}
`,
popoverMenuNav: css`
${antCls}-menu-item,
${antCls}-menu-submenu {
text-align: left;
}
${antCls}-menu-item-group-title {
padding-left: 24px;
}
${antCls}-menu-item-group-list {
padding: 0 16px;
}
${antCls}-menu-item,
a {
color: #333;
}
`,
};
};
export interface NavigationProps extends SharedProps {
isMobile: boolean;
pathname: string;
isClient: boolean;
responsive: null | 'narrow' | 'crowded';
location: { pathname: string; query: any };
directionText: string;
showTechUIButton: boolean;
onLangChange: () => void;
@ -24,22 +90,29 @@ export interface NavigationProps extends SharedProps {
export default ({
isZhCN,
isClient,
isMobile,
pathname,
responsive,
location,
directionText,
showTechUIButton,
onLangChange,
onDirectionChange,
}: NavigationProps) => {
const { pathname, search } = useLocation();
const style = useStyle();
const menuMode = isMobile ? 'inline' : 'horizontal';
const module = pathname.split('/').slice(0, -1).join('/');
const module = pathname
.split('/')
.filter((path) => path)
.slice(0, -1)
.join('/');
let activeMenuItem = module || 'home';
if (location.pathname === 'changelog' || location.pathname === 'changelog-cn') {
if (pathname.startsWith('/changelog')) {
activeMenuItem = 'docs/react';
} else if (location.pathname === 'docs/resources' || location.pathname === 'docs/resources-cn') {
} else if (pathname.startsWith('/docs/resources')) {
activeMenuItem = 'docs/resources';
}
@ -86,7 +159,7 @@ export default ({
const items: MenuProps['items'] = [
{
label: (
<Link to={utils.getLocalizedPathname('/docs/spec/introduce', isZhCN, location.query)}>
<Link to={utils.getLocalizedPathname('/docs/spec/introduce', isZhCN, search)}>
<FormattedMessage id="app.header.menu.spec" />
</Link>
),
@ -94,15 +167,15 @@ export default ({
},
{
label: (
<Link to={utils.getLocalizedPathname('/docs/react/introduce', isZhCN, location.query)}>
<FormattedMessage id="app.header.menu.documentation" />
<Link to={utils.getLocalizedPathname('/docs/react/introduce', isZhCN, search)}>
<FormattedMessage id="app.header.menu.development" />
</Link>
),
key: 'docs/react',
},
{
label: (
<Link to={utils.getLocalizedPathname('/components/overview/', isZhCN, location.query)}>
<Link to={utils.getLocalizedPathname('/components/overview/', isZhCN, search)}>
<FormattedMessage id="app.header.menu.components" />
</Link>
),
@ -110,7 +183,7 @@ export default ({
},
{
label: (
<Link to={utils.getLocalizedPathname('/docs/resources', isZhCN, location.query)}>
<Link to={utils.getLocalizedPathname('/docs/resources', isZhCN, search)}>
<FormattedMessage id="app.header.menu.resource" />
</Link>
),
@ -127,7 +200,7 @@ export default ({
}
: null,
isZhCN &&
typeof window !== 'undefined' &&
isClient &&
window.location.host !== 'ant-design.antgroup.com' &&
window.location.host !== 'ant-design.gitee.io'
? {
@ -169,7 +242,7 @@ export default ({
className={classNames('menu-site')}
mode={menuMode}
selectedKeys={[activeMenuItem]}
id="nav"
css={style.nav}
disabledOverflow
items={items}
/>

View File

@ -0,0 +1,480 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { FormattedMessage, useIntl } from 'dumi';
import DumiSearchBar from 'dumi/theme-default/slots/SearchBar';
import classNames from 'classnames';
import { Button, Col, Modal, Popover, Row, Select, Typography } from 'antd';
import { MenuOutlined } from '@ant-design/icons';
import canUseDom from 'rc-util/lib/Dom/canUseDom';
import type { DirectionType } from 'antd/es/config-provider';
import * as utils from '../../utils';
import { getThemeConfig, ping } from '../../utils';
import packageJson from '../../../../package.json';
import Logo from './Logo';
import More from './More';
import Navigation from './Navigation';
import Github from './Github';
import type { SiteContextProps } from '../SiteContext';
import SiteContext from '../SiteContext';
import { useLocation, useNavigate } from 'dumi';
import { ClassNames, css } from '@emotion/react';
import useSiteToken from '../../../hooks/useSiteToken';
import useLocale from '../../../hooks/useLocale';
const RESPONSIVE_XS = 1120;
const RESPONSIVE_SM = 1200;
const { Option } = Select;
const antdVersion: string = packageJson.version;
const locales = {
cn: {
title: '🎉🎉🎉 Ant Design 5.0 发布! 🎉🎉🎉',
ok: '知道了',
},
en: {
title: '🎉🎉🎉 Ant Design 5.0 is released! 🎉🎉🎉',
ok: 'Got it',
},
};
const useStyle = () => {
const { token } = useSiteToken();
const searchIconColor = '#ced4d9';
return {
header: css`
position: relative;
z-index: 10;
max-width: 100%;
background: ${token.colorBgContainer};
box-shadow: ${token.boxShadow};
@media only screen and (max-width: ${token.mobileMaxWidth}px) {
text-align: center;
}
.nav-search-wrapper {
flex: auto;
display: flex;
}
.dumi-default-search-bar {
border-inline-start: 1px solid rgba(0, 0, 0, 0.06);
> svg {
width: 14px;
fill: ${searchIconColor};
}
> input {
height: 22px;
border: 0;
&:focus {
box-shadow: none;
}
&::placeholder {
color: ${searchIconColor};
}
}
.dumi-default-search-shortcut {
color: ${searchIconColor};
background-color: rgba(150, 150, 150, 0.06);
border-color: rgba(100, 100, 100, 0.2);
border-radius: 4px;
}
.dumi-default-search-popover {
inset-inline-start: 11px;
inset-inline-end: unset;
&::before {
inset-inline-start: 100px;
inset-inline-end: unset;
}
}
}
`,
menuRow: css`
display: flex;
align-items: center;
margin: 0;
> * {
flex: none;
margin: 0 12px 0 0;
&:last-child {
margin-right: 40px;
}
}
${token.antCls}-row-rtl & {
> * {
&:last-child {
margin-right: 12px;
margin-left: 40px;
}
}
}
`,
headerButton: css`
color: ${token.colorText};
border-color: ${token.colorBorder};
`,
popoverMenu: {
width: 300,
[`${token.antCls}-popover-inner-content`]: {
padding: 0,
},
},
};
};
export interface HeaderProps {
changeDirection: (direction: DirectionType) => void;
}
let docsearch: any;
const triggerDocSearchImport = () => {
if (docsearch) {
return Promise.resolve();
}
// @ts-ignore
return import('docsearch.js').then((ds) => {
docsearch = ds.default;
});
};
const V5_NOTIFICATION = 'antd@4.0.0-notification-sent';
const SHOULD_OPEN_ANT_DESIGN_MIRROR_MODAL = 'ANT_DESIGN_DO_NOT_OPEN_MIRROR_MODAL';
function disableAntdMirrorModal() {
window.localStorage.setItem(SHOULD_OPEN_ANT_DESIGN_MIRROR_MODAL, 'true');
}
function shouldOpenAntdMirrorModal() {
return !window.localStorage.getItem(SHOULD_OPEN_ANT_DESIGN_MIRROR_MODAL);
}
interface HeaderState {
menuVisible: boolean;
windowWidth: number;
searching: boolean;
showTechUIButton: boolean;
}
// ================================= Header =================================
const Header: React.FC<HeaderProps> = (props) => {
const intl = useIntl();
const { changeDirection } = props;
const [isClient, setIsClient] = React.useState(false);
const [locale, lang] = useLocale(locales);
const { token } = useSiteToken();
const [notify, setNotify] = React.useState<null | boolean>(null);
// ========================= 发布通知 开始 =========================
React.useEffect(() => {
if (utils.isLocalStorageNameSupported()) {
// 大版本发布后全局弹窗提示
// 1. 点击『知道了』之后不再提示
// 2. 超过截止日期后不再提示
if (
localStorage.getItem(V5_NOTIFICATION) !== 'true' &&
Date.now() < new Date('2022/12/31').getTime()
) {
setNotify(true);
return;
}
}
setNotify(false);
}, []);
function onClose() {
setNotify(false);
localStorage.setItem(V5_NOTIFICATION, 'true');
}
// ========================= 发布通知 结束 =========================
const themeConfig = getThemeConfig();
const [headerState, setHeaderState] = useState<HeaderState>({
menuVisible: false,
windowWidth: 1400,
searching: false,
showTechUIButton: false,
});
const { direction, isMobile } = useContext<SiteContextProps>(SiteContext);
const pingTimer = useRef<NodeJS.Timeout | null>(null);
const location = useLocation();
const { pathname, search } = location;
const navigate = useNavigate();
const style = useStyle();
const handleHideMenu = useCallback(() => {
setHeaderState((prev) => ({ ...prev, menuVisible: false }));
}, []);
const onWindowResize = useCallback(() => {
setHeaderState((prev) => ({ ...prev, windowWidth: window.innerWidth }));
}, []);
const onTriggerSearching = useCallback((searching: boolean) => {
setHeaderState((prev) => ({ ...prev, searching }));
}, []);
const handleShowMenu = useCallback(() => {
setHeaderState((prev) => ({ ...prev, menuVisible: true }));
}, []);
const onMenuVisibleChange = useCallback((visible: boolean) => {
setHeaderState((prev) => ({ ...prev, menuVisible: visible }));
}, []);
const onDirectionChange = useCallback(() => {
changeDirection?.(direction !== 'rtl' ? 'rtl' : 'ltr');
}, [direction]);
useEffect(() => {
handleHideMenu();
}, [location]);
useEffect(() => {
setIsClient(typeof window !== 'undefined');
onWindowResize();
window.addEventListener('resize', onWindowResize);
pingTimer.current = ping((status) => {
if (status !== 'timeout' && status !== 'error') {
setHeaderState((prev) => ({ ...prev, showTechUIButton: true }));
if (
// process.env.NODE_ENV === 'production' &&
window.location.host !== 'ant-design.antgroup.com' &&
shouldOpenAntdMirrorModal()
) {
Modal.confirm({
title: '提示',
content: '内网用户推荐访问国内镜像以获得极速体验~',
okText: '🚀 立刻前往',
cancelText: '不再弹出',
closable: true,
zIndex: 99999,
onOk() {
window.open('https://ant-design.antgroup.com', '_self');
disableAntdMirrorModal();
},
onCancel() {
disableAntdMirrorModal();
},
});
}
}
});
return () => {
window.removeEventListener('resize', onWindowResize);
if (pingTimer.current) {
clearTimeout(pingTimer.current);
}
};
}, []);
// eslint-disable-next-line class-methods-use-this
const handleVersionChange = useCallback((url: string) => {
const currentUrl = window.location.href;
const currentPathname = window.location.pathname;
if (/overview/.test(currentPathname) && /0?[1-39][0-3]?x/.test(url)) {
window.location.href = currentUrl
.replace(window.location.origin, url)
.replace(/\/components\/overview/, `/docs${/0(9|10)x/.test(url) ? '' : '/react'}/introduce`)
.replace(/\/$/, '');
return;
}
window.location.href = currentUrl.replace(window.location.origin, url);
}, []);
const onLangChange = useCallback(() => {
const currentProtocol = `${window.location.protocol}//`;
const currentHref = window.location.href.slice(currentProtocol.length);
if (utils.isLocalStorageNameSupported()) {
localStorage.setItem('locale', utils.isZhCN(pathname) ? 'en-US' : 'zh-CN');
}
window.location.href =
currentProtocol +
currentHref.replace(
window.location.pathname,
utils.getLocalizedPathname(pathname, !utils.isZhCN(pathname), search).pathname,
);
}, [location]);
const nextDirectionText = useMemo<string>(
() => (direction !== 'rtl' ? 'RTL' : 'LTR'),
[direction],
);
const getDropdownStyle = useMemo<React.CSSProperties>(
() => (direction === 'rtl' ? { direction: 'ltr', textAlign: 'right' } : {}),
[direction],
);
const { menuVisible, windowWidth, searching, showTechUIButton } = headerState;
const docVersions: Record<string, string> = {
[antdVersion]: antdVersion,
...themeConfig?.docVersions,
};
const versionOptions = Object.keys(docVersions).map((version) => (
<Option value={docVersions[version]} key={version}>
{version}
</Option>
));
const isHome = ['', 'index', 'index-cn'].includes(pathname);
const isZhCN = lang === 'cn';
const isRTL = direction === 'rtl';
let responsive: null | 'narrow' | 'crowded' = null;
if (windowWidth < RESPONSIVE_XS) {
responsive = 'crowded';
} else if (windowWidth < RESPONSIVE_SM) {
responsive = 'narrow';
}
const headerClassName = classNames({
clearfix: true,
'home-header': isHome,
});
const sharedProps = {
isZhCN,
isRTL,
isClient,
};
const navigationNode = (
<Navigation
key="nav"
{...sharedProps}
responsive={responsive}
isMobile={isMobile}
showTechUIButton={showTechUIButton}
directionText={nextDirectionText}
onLangChange={onLangChange}
onDirectionChange={onDirectionChange}
/>
);
let menu: (React.ReactElement | null)[] = [
navigationNode,
<Popover
open={!!notify}
title={locale.title}
content={
<Typography style={{ marginTop: token.marginXS }}>
{lang === 'cn' ? (
<>
<div>
{' '}
<Typography.Link
target="_blank"
href="https://github.com/ant-design/ant-design/issues/38463"
>
Github Issue
</Typography.Link>{' '}
</div>
<div> v4 </div>
</>
) : (
<>
<div>
If you find any official site problem. Please feel free to report on{' '}
<Typography.Link
target="_blank"
href="https://github.com/ant-design/ant-design/issues/38463"
>
Github Issue
</Typography.Link>
.
</div>
<p>Click above Select to switch to v4 docs.</p>
</>
)}
</Typography>
}
>
<Select
key="version"
className="version"
size="small"
defaultValue={antdVersion}
onChange={handleVersionChange}
dropdownStyle={getDropdownStyle}
getPopupContainer={(trigger) => trigger.parentNode}
onClick={onClose}
>
{versionOptions}
</Select>
</Popover>,
<Button size="small" onClick={onLangChange} css={style.headerButton} key="lang-button">
<FormattedMessage id="app.header.lang" />
</Button>,
<Button
size="small"
onClick={onDirectionChange}
css={style.headerButton}
key="direction-button"
>
{nextDirectionText}
</Button>,
<More key="more" {...sharedProps} />,
<Github key="github" responsive={responsive} />,
];
if (windowWidth < RESPONSIVE_XS) {
menu = searching ? [] : [navigationNode];
} else if (windowWidth < RESPONSIVE_SM) {
menu = searching ? [] : menu;
}
const colProps = isHome
? [{ flex: 'none' }, { flex: 'auto' }]
: [
{ xxl: 4, xl: 5, lg: 6, md: 6, sm: 24, xs: 24 },
{ xxl: 20, xl: 19, lg: 18, md: 18, sm: 0, xs: 0 },
];
return (
<header css={style.header} className={headerClassName}>
{isMobile && (
<ClassNames>
{({ css }) => (
<Popover
overlayClassName={css(style.popoverMenu)}
placement="bottomRight"
content={menu}
trigger="click"
open={menuVisible}
arrowPointAtCenter
onOpenChange={onMenuVisibleChange}
>
<MenuOutlined className="nav-phone-icon" onClick={handleShowMenu} />
</Popover>
)}
</ClassNames>
)}
<Row style={{ flexFlow: 'nowrap', height: 64 }}>
<Col {...colProps[0]}>
<Logo {...sharedProps} location={location} />
</Col>
<Col {...colProps[1]} css={style.menuRow}>
<div className="nav-search-wrapper">
<DumiSearchBar />
</div>
{!isMobile && menu}
</Col>
</Row>
</header>
);
};
export default Header;

View File

@ -1,4 +1,5 @@
export interface SharedProps {
isZhCN: boolean;
isRTL: boolean;
isClient: boolean;
}

View File

@ -0,0 +1,158 @@
import React, { type FC, useContext } from 'react';
import { useSidebarData } from 'dumi';
import { Affix, Col, Menu } from 'antd';
import MobileMenu from 'rc-drawer';
import SiteContext from '../SiteContext';
import useMenu from '../../../hooks/useMenu';
import useSiteToken from '../../../hooks/useSiteToken';
import { css } from '@emotion/react';
const useStyle = () => {
const { token } = useSiteToken();
const { antCls, fontFamily, colorSplit } = token;
return {
asideContainer: css`
min-height: 100%;
padding-bottom: 48px;
font-family: Avenir, ${fontFamily}, sans-serif;
&${antCls}-menu-inline {
${antCls}-menu-submenu-title h4,
> ${antCls}-menu-item,
${antCls}-menu-item a {
overflow: hidden;
font-size: 14px;
text-overflow: ellipsis;
}
> ${antCls}-menu-item-group > ${antCls}-menu-item-group-title {
margin-top: 16px;
margin-bottom: 16px;
font-size: 13px;
&::after {
position: relative;
top: 12px;
display: block;
width: calc(100% - 20px);
height: 1px;
background: ${colorSplit};
content: '';
}
}
> ${antCls}-menu-item,
> ${antCls}-menu-submenu
> ${antCls}-menu-submenu-title,
> ${antCls}-menu-item-group
> ${antCls}-menu-item-group-title,
> ${antCls}-menu-item-group
> ${antCls}-menu-item-group-list
> ${antCls}-menu-item,
&${antCls}-menu-inline
> ${antCls}-menu-item-group
> ${antCls}-menu-item-group-list
> ${antCls}-menu-item {
padding-left: 40px !important;
${antCls}-row-rtl & {
padding-right: 40px !important;
padding-left: 16px !important;
}
}
// Nest Category > Type > Article
&${antCls}-menu-inline {
${antCls}-menu-item-group-title {
padding-left: 60px;
margin-left: 4px;
${antCls}-row-rtl & {
padding-right: 60px;
padding-left: 16px;
}
}
${antCls}-menu-item-group-list > ${antCls}-menu-item {
padding-left: 80px !important;
${antCls}-row-rtl & {
padding-right: 80px !important;
padding-left: 16px !important;
}
}
}
${antCls}-menu-item-group:first-child {
${antCls}-menu-item-group-title {
margin-top: 0;
}
}
}
a[disabled] {
color: #ccc;
}
.chinese {
margin-left: 6px;
font-weight: normal;
font-size: 12px;
opacity: 0.67;
}
`,
mainMenu: css`
z-index: 1;
.main-menu-inner {
height: 100%;
max-height: 100vh;
overflow: hidden;
}
&:hover .main-menu-inner {
overflow-y: auto;
}
> div,
> div > div {
height: 100%;
}
`,
};
};
const Sidebar: FC = () => {
const sidebarData = useSidebarData();
const { isMobile } = useContext(SiteContext);
const styles = useStyle();
const [menuItems, selectedKey] = useMenu();
const menuChild = (
<Menu
items={menuItems}
inlineIndent={30}
css={styles.asideContainer}
mode="inline"
selectedKeys={[selectedKey]}
defaultOpenKeys={sidebarData?.map(({ title }) => title).filter((item) => item) as string[]}
/>
);
return isMobile ? (
<MobileMenu key="Mobile-menu">{menuChild}</MobileMenu>
) : (
<Col xxl={4} xl={5} lg={6} md={6} sm={24} xs={24} css={styles.mainMenu}>
<Affix>
<section style={{ width: '100%' }} className="main-menu-inner">
{menuChild}
</section>
</Affix>
</Col>
);
};
export default Sidebar;

View File

@ -4,9 +4,6 @@ import type { DirectionType } from 'antd/es/config-provider';
export interface SiteContextProps {
isMobile: boolean;
direction: DirectionType;
theme?: string;
setTheme?: (theme: string, persist?: boolean) => void;
setIframeTheme?: (iframeNode: HTMLIFrameElement, theme: string) => void;
}
const SiteContext = React.createContext<SiteContextProps>({

View File

@ -0,0 +1,11 @@
import { createContext } from 'react';
import { ThemeConfig } from 'antd/es/config-provider/context';
export type ThemeContextProps = {
theme: ThemeConfig;
setTheme: (theme: ThemeConfig) => void;
};
const ThemeContext = createContext<ThemeContextProps>({ theme: {}, setTheme: () => {} });
export default ThemeContext;

View File

@ -1,3 +1,3 @@
import 'react-github-button/assets/style.css';
import 'docsearch.js/dist/cdn/docsearch.css';
import './index.less';
import 'rc-footer/assets/index.css';

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
categoryOrder: {
'Ant Design': 0,
全局样式: 1,
@ -43,12 +43,13 @@ module.exports = {
'Template Document': 3,
},
docVersions: {
'3.x': 'http://3x.ant.design',
'2.x': 'http://2x.ant.design',
'1.x': 'http://1x.ant.design',
'0.12.x': 'http://012x.ant.design',
'0.11.x': 'http://011x.ant.design',
'0.10.x': 'http://010x.ant.design',
'0.9.x': 'http://09x.ant.design',
'4.x': '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

@ -1,12 +1,16 @@
import flattenDeep from 'lodash/flattenDeep';
import flatten from 'lodash/flatten';
import themeConfig from '../../themeConfig';
import flattenDeep from 'lodash/flattenDeep';
import themeConfig from './themeConfig';
interface Meta {
skip?: boolean;
category?: any;
type?: any;
title?: any;
subtitle?: string;
tag?: string;
path?: string;
cover?: string;
order?: number;
children?: Meta[];
}
@ -118,7 +122,7 @@ export function isZhCN(pathname: string) {
export function getLocalizedPathname(
path: string,
zhCN?: boolean,
query?: { [key: string]: any },
search?: string,
hash?: {
zhCN?: string;
enUS?: string;
@ -142,7 +146,7 @@ export function getLocalizedPathname(
fullPath += `#${localHash}`;
}
return { pathname: fullPath, query };
return { pathname: fullPath, search };
}
export function ping(callback: (status: string) => void) {

15
.dumi/tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": "../.dumi/tmp/tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@emotion/react",
"baseUrl": "../",
"paths": {
"@@/*": [".dumi/tmp/*"],
"antd": ["components/index.tsx"],
"antd/es/*": ["components/*"],
"dumi/theme/*": [".dumi/theme/*"]
}
},
"include": ["./**/*", "../site/theme/template/Content"]
}

99
.dumirc.ts Normal file
View File

@ -0,0 +1,99 @@
import { defineConfig } from 'dumi';
import path from 'path';
import rehypeAntd from './.dumi/rehypeAntd';
import { version } from './package.json';
export default defineConfig({
conventionRoutes: {
// to avoid generate routes for .dumi/pages/index/components/xx
exclude: [new RegExp('index/components/')],
},
ssr: process.env.NODE_ENV === 'production' ? {} : false,
hash: true,
outputPath: '_site',
favicons: ['https://gw.alipayobjects.com/zos/rmsportal/rlpTLlbMzTNYuZGGCVYM.png'],
resolve: {
docDirs: [{ type: 'doc', dir: 'docs' }],
atomDirs: [{ type: 'component', dir: 'components' }],
codeBlockMode: 'passive',
},
locales: [
{ id: 'en-US', name: 'English', suffix: '' },
{ id: 'zh-CN', name: '中文', suffix: '-cn' },
],
define: {
antdReproduceVersion: version,
},
alias: {
'antd/lib': path.join(__dirname, 'components'),
'antd/es': path.join(__dirname, 'components'),
'antd/locale': path.join(__dirname, 'components/locale'),
// Change antd from `index.js` to `.dumi/theme/antd.js` to remove deps of root style
antd: require.resolve('./.dumi/theme/antd.js'),
},
extraRehypePlugins: [rehypeAntd],
extraBabelPresets: ['@emotion/babel-preset-css-prop'],
mfsu: false,
metas: [{ name: 'theme-color', content: '#1677ff' }],
analytics: {
ga_v2: 'UA-72788897-1',
},
headScripts: [
`
(function () {
function isLocalStorageNameSupported() {
var testKey = 'test';
var storage = window.localStorage;
try {
storage.setItem(testKey, '1');
storage.removeItem(testKey);
return true;
} catch (error) {
return false;
}
}
// 优先级提高到所有静态资源的前面,语言不对,加载其他静态资源没意义
var pathname = location.pathname;
function isZhCN(pathname) {
return /-cn\\/?$/.test(pathname);
}
function getLocalizedPathname(path, zhCN) {
var pathname = path.indexOf('/') === 0 ? path : '/' + path;
if (!zhCN) {
// to enUS
return /\\/?index(-cn)?/.test(pathname) ? '/' : pathname.replace('-cn', '');
} else if (pathname === '/') {
return '/index-cn';
} else if (pathname.indexOf('/') === pathname.length - 1) {
return pathname.replace(/\\/$/, '-cn/');
}
return pathname + '-cn';
}
// 兼容旧的 URL \`?locale=...\`
var queryString = location.search;
if (queryString) {
var isZhCNConfig = queryString.indexOf('zh-CN') > -1;
if (isZhCNConfig && !isZhCN(pathname)) {
location.pathname = getLocalizedPathname(pathname, isZhCNConfig);
}
}
// 首页无视链接里面的语言设置 https://github.com/ant-design/ant-design/issues/4552
if (isLocalStorageNameSupported() && (pathname === '/' || pathname === '/index-cn')) {
var 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(pathname)) {
location.pathname = getLocalizedPathname(pathname, lang === 'zh-CN');
}
}
document.documentElement.className += isZhCN(pathname) ? 'zh-cn' : 'en-us';
})();
`,
],
});

View File

@ -1,26 +1,16 @@
components/**/*.js
components/**/*.jsx
components/version/token.tsx
!components/*/__tests__/**/*.js
!components/*/demo/*
!components/*/demo/*.md
!.*.js
~*
# Docs templates
site/theme/template/Color/ColorPicker.jsx
site/theme/template/IconDisplay/*.js
site/theme/template/IconDisplay/*.jsx
site/theme/template/IconDisplay/fields.js
site/theme/template/Home/**/*.jsx
site/theme/template/utils.jsx
site/theme/template/Layout/**/*.jsx
site/theme/template/Layout/Footer.jsx
site/theme/template/Content/Article.jsx
site/theme/template/Content/EditButton.jsx
site/theme/template/Resources/*.jsx
site/theme/template/Resources/**/*.jsx
site/theme/template/NotFound.jsx
typings
es/**/*
lib/**/*
locale
server
.dumi/tmp
node_modules
_site
dist

View File

@ -20,6 +20,9 @@ module.exports = {
version: 'detect',
},
polyfills: ['Promise', 'URL'],
'import/resolver': {
typescript: {},
},
},
parser: '@typescript-eslint/parser',
plugins: ['react', 'babel', 'jest', '@typescript-eslint', 'react-hooks', 'unicorn', 'markdown'],
@ -83,6 +86,18 @@ module.exports = {
'react/no-unstable-nested-components': 0,
},
},
{
files: ['components/**/demo/*.tsx'],
rules: {
'import/no-extraneous-dependencies': 0,
'no-console': 0,
'compat/compat': 0,
'react/no-unstable-nested-components': 0,
'jsx-a11y/control-has-associated-label': 0,
'class-methods-use-this': 0,
'react/no-access-state-in-setstate': 0,
},
},
],
rules: {
'react/jsx-one-expression-per-line': 0,

View File

@ -1,99 +0,0 @@
name: 🇨🇳 报告缺陷beta
description: 使用中文 Bug 报告模板提交一个缺陷,当然你也可以用 https://new-issue.ant.design
title: "[Bug] 请替换成合适的标题"
labels: ["🐛+Bug"]
body:
- type: markdown
attributes:
value: |
除了此表单,你也可以使用 [Ant Design Issue Helper](https://new-issue.ant.design/) 来新建 issue。
Ant Design 的 issue 列表只接受 [Bug 报告](https://github.com/ant-design/ant-design/issues/new?assignees=&labels=%F0%9F%90%9B+Bug&template=template-1-bug-report.yml&title=%5BBug%5D+) 或是 [新功能请求](https://github.com/ant-design/ant-design/issues/new?assignees=&labels=%F0%9F%92%A1%2BFeature%2BRequest&template=template-2-feature-request.yml&title=%5BFeature+Request%5D+) (Feature Request)。这意味着我们不接受用法问题How to 类问题)。如果你开的 issue 不符合规定,它将会被立刻关闭。[为什么要这么严格?](https://github.com/ant-design/antd-issue-helper/blob/master/src/locales/zh/introModal.md)
对于使用中遇到的问题,请使用以下资源:
- 将用法类问题发到 [GitHub 讨论区](https://github.com/ant-design/ant-design/discussions) 而不是 Issue。
- 仔细阅读 [使用文档](https://ant.design/docs/react/introduce-cn) 和 [组件文档](https://ant.design/components/overview-cn/)。
- 提问前确保你在 [常见问题](https://ant.design/docs/react/faq-cn) 和 [更新日志](https://ant.design/changelog-cn) 中搜索过。
- 在 [StackOverflow](https://stackoverflow.com/questions/tagged/antd) (英文) 或是 [SegmentFault](https://segmentfault.com/t/antd)(中文)搜索和提问。
最后,在开 issue 前,可以先搜索一下以往的旧 issue - 你遇到的问题可能已经有人提了,也可能已经在最新版本中被修正。注意:如果你发现一个已经关闭的旧 issue 在最新版本中仍然存在,请不要在旧 issue 下面留言,而应该用下面的表单开一个新的 issue。
---
- type: input
id: reproduce
attributes:
label: 重现链接
description: 请提供一个尽可能精简的 [codesandbox](https://u.ant.design/codesandbox-repro) 或 [GitHub 仓库](https://github.com/ant-design/create-react-app-antd/) 的链接。[什么是最小化重现,为什么这是必需的?](https://github.com/ant-design/antd-issue-helper/blob/master/src/locales/zh/reproModal.md)
validations:
required: true
- type: input
id: version
attributes:
label: antd 版本
description: 你正在使用的 antd 版本是多少?(注意不是 package.json 里的版本,而是 node_modules/antd 里实际安装的版本)
placeholder: 如 4.16.13
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: 重现步骤
description: 请提供一个傻瓜式的操作步骤,方便我们无脑重现问题。
placeholder: |
比如:
1. 点击重现链接里的主按钮
2. 等待五秒钟
3. 观察按钮的边框
validations:
required: true
- type: textarea
id: expected
attributes:
label: 期望的结果是什么?
placeholder: 比如:按钮样式正常,边框一直存在
validations:
required: true
- type: textarea
id: reality
attributes:
label: 实际的结果是什么?
placeholder: 比如:按钮样式异常,边框突然丢失
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: 浏览器
description: 在哪些浏览器上能重现这个问题?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Edge
- Internet Explorer
- Others
- type: input
id: browsers-version
attributes:
label: 浏览器版本
placeholder: 如:版本 95.0.4638.69(正式版本) (x86_64)
- type: dropdown
id: os
attributes:
label: 操作系统
description: 使用的操作系统是?
options:
- Windows
- MacOS
- iOS
- Android
- Others
- type: input
id: hidden-tag
attributes:
label: 其他
description: 不要删除预填内容,否则 issue 会被自动关闭。
value: <!-- generated by ant-design-issue-helper. DO NOT REMOVE -->

View File

@ -1,50 +0,0 @@
name: 🇨🇳 功能需求beta
description: 使用中文 Bug 报告模板提交一个功能需求,当然你也可以用 https://new-issue.ant.design
title: "[Feature Request] 请替换成合适的标题"
labels: ["💡+Feature+Request"]
body:
- type: markdown
attributes:
value: |
除了此表单,你也可以使用 [Ant Design Issue Helper](https://new-issue.ant.design/) 来新建 issue。
Ant Design 的 issue 列表只接受 [Bug 报告](https://github.com/ant-design/ant-design/issues/new?assignees=&labels=%F0%9F%90%9B+Bug&template=template-1-bug-report.yml&title=%5BBug%5D+) 或是 [新功能请求](https://github.com/ant-design/ant-design/issues/new?assignees=&labels=%F0%9F%92%A1%2BFeature%2BRequest&template=template-2-feature-request.yml&title=%5BFeature+Request%5D+) (Feature Request)。这意味着我们不接受用法问题How to 类问题)。如果你开的 issue 不符合规定,它将会被立刻关闭。[为什么要这么严格?](https://github.com/ant-design/antd-issue-helper/blob/master/src/locales/zh/introModal.md)
对于使用中遇到的问题,请使用以下资源:
- 将用法类问题发到 [GitHub 讨论区](https://github.com/ant-design/ant-design/discussions) 而不是 Issue。
- 仔细阅读 [使用文档](https://ant.design/docs/react/introduce-cn) 和 [组件文档](https://ant.design/components/overview-cn/)。
- 提问前确保你在 [常见问题](https://ant.design/docs/react/faq-cn) 和 [更新日志](https://ant.design/changelog-cn) 中搜索过。
- 在 [StackOverflow](https://stackoverflow.com/questions/tagged/antd) (英文) 或是 [SegmentFault](https://segmentfault.com/t/antd)(中文)搜索和提问。
最后,在开 issue 前,可以先搜索一下以往的旧 issue - 你遇到的问题可能已经有人提了,也可能已经在最新版本中被修正。注意:如果你发现一个已经关闭的旧 issue 在最新版本中仍然存在,请不要在旧 issue 下面留言,而应该用下面的表单开一个新的 issue。
---
- type: textarea
id: needs
attributes:
label: 这个功能解决了什么问题?
description: |
请尽可能详尽地说明这个需求的用例和场景。最重要的是:解释清楚是怎样的用户体验需求催生了这个功能上的需求。
Ant Design 的一个重要设计原则是保持 API 的简洁和直接。通常来说,我们只考虑添加在现有的 API 下无法轻松实现的功能。新功能的用例也应当足够常见。
validations:
required: true
- type: textarea
id: api
attributes:
label: 你期望的 API 是怎样的?
description: |
描述一下你期望这个新功能的 API 是如何使用的,并提供一些代码示例。请用 Markdown 格式化你的代码片段。
如果有新的 UI 界面,最好截图展示你期望的界面是什么样的?
placeholder: |
```jsx
<Drawer extra={<Button />} {...props} />
```
validations:
required: true
- type: input
id: hidden-tag
attributes:
label: 其他
description: 不要删除预填内容,否则 issue 会被自动关闭。
value: <!-- generated by ant-design-issue-helper. DO NOT REMOVE -->

View File

@ -38,9 +38,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hello @${{ github.event.issue.user.login }}. Please provide a online reproduction by forking this link https://u.ant.design/codesandbox-repro or a minimal GitHub repository. Issues labeled by `Need Reproduce` will be closed if no activities in 3 days.
Hello @${{ github.event.issue.user.login }}. Please provide a online reproduction by forking codesandbox of [antd@5.x](https://u.ant.design/codesandbox-repro) or [antd@4.x](https://u.ant.design/codesandbox-repro-4x), or provide a minimal GitHub repository. Issues labeled by `Need Reproduce` will be closed if no activities in 3 days.
你好 @${{ github.event.issue.user.login }}, 我们需要你提供一个在线的重现实例以便于我们帮你排查问题。你可以通过点击 [此处](https://u.ant.design/codesandbox-repro) 创建一个 codesandbox 或者提供一个最小化的 GitHub 仓库。3 天内未跟进此 issue 将会被自动关闭。
你好 @${{ github.event.issue.user.login }}, 我们需要你提供一个在线的重现实例以便于我们帮你排查问题。你可以通过点击这里创建一个 [antd@5.x](https://u.ant.design/codesandbox-repro) 或 [antd@4.x](https://u.ant.design/codesandbox-repro-4x) 的 codesandbox或者提供一个最小化的 GitHub 仓库。3 天内未跟进此 issue 将会被自动关闭。
![](https://gw.alipayobjects.com/zos/antfincdn/y9kwg7DVCd/reproduce.gif)

View File

@ -59,15 +59,15 @@ jobs:
* Ant Motion: https://motion.ant.design
## 国内镜像
* 官方镜像: https://ant-design.antgroup.com/
* 官方镜像: https://ant-design.antgroup.com
* Gitee 镜像: http://ant-design.gitee.io
* Ant Design 3.x: http://ant-design-3x.gitee.io
* Ant Design 2.x: http://ant-design-2x.gitee.io
* Ant Design 1.x: http://ant-design-1x.gitee.io
* Ant Design Pro: http://ant-design-pro.gitee.io
* Ant Design Mobile: http://antd-mobile.gitee.io
* Ant Motion: http://ant-motion.gitee.io
* Ant Design Pro Preview : https://prosite.z23.web.core.windows.net
* Ant Design 4.x: https://4x-ant-design.antgroup.com
* Ant Design 3.x: https://ant-design-3x.gitee.io
* Ant Design 2.x: https://ant-design-2x.gitee.io
* Ant Design 1.x: https://ant-design-1x.gitee.io
* Ant Design Pro: https://ant-design-pro.gitee.io
* Ant Design Mobile: https://antd-mobile.gitee.io
* Ant Motion: https://ant-motion.gitee.io
- name: check ie
if: contains(github.event.issue.body, 'ant-design-issue-helper') == true && contains(github.event.issue.title, 'IE9') == true || contains(github.event.issue.title, 'IE 9') == true || contains(github.event.issue.title, 'IE10') == true || contains(github.event.issue.title, 'IE 10') == true || contains(github.event.issue.title, 'IE11') == true || contains(github.event.issue.title, 'IE 11') == true || contains(github.event.issue.title, 'Internet Explorer') == true || contains(github.event.issue.body, 'IE9') == true || contains(github.event.issue.body, 'IE 9') == true || contains(github.event.issue.body, 'IE10') == true || contains(github.event.issue.body, 'IE 10') == true || contains(github.event.issue.body, 'IE11') == true || contains(github.event.issue.body, 'IE 11') == true || contains(github.event.issue.body, 'Internet Explorer') == true

View File

@ -74,6 +74,7 @@ jobs:
run: npm run site
env:
SITE_ENV: development
NODE_OPTIONS: --max_old_space_size=4096
- name: upload site artifact
uses: actions/upload-artifact@v3

View File

@ -10,7 +10,7 @@ permissions:
jobs:
setup:
runs-on: ubuntu-latest
if: github.event.ref_type == 'tag' && (contains(github.event.ref, '-') == false)
if: github.event.ref_type == 'tag' && (contains(github.event.ref, '-') == false || startsWith(github.event.ref, '5.0.0'))
steps:
- name: checkout
uses: actions/checkout@v3
@ -66,5 +66,6 @@ jobs:
- name: deploy
uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
deploy_key: ${{ secrets.NEXT_SITE_DEPLOY }}
external_repository: ant-design/next.ant.design
publish_dir: ./_site

View File

@ -67,28 +67,6 @@ jobs:
run: npm run lint
needs: setup
tsx-demo:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: restore cache from package-lock.json
uses: actions/cache@v3
with:
path: package-temp-dir
key: lock-${{ github.sha }}
- name: restore cache from node_modules
uses: actions/cache@v3
with:
path: node_modules
key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }}
- name: tsx-demo
run: npm run check-ts-demo
needs: setup
check_metadata:
runs-on: ubuntu-latest
steps:
@ -143,92 +121,6 @@ jobs:
NODE_OPTIONS: --max_old_space_size=4096
needs: setup
############################### Style ################################
style-compile:
name: es style compile
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: restore cache from package-lock.json
uses: actions/cache@v3
with:
path: package-temp-dir
key: lock-${{ github.sha }}
- name: restore cache from node_modules
uses: actions/cache@v3
with:
path: node_modules
key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }}
- name: restore cache from lib
uses: actions/cache@v3
with:
path: lib
key: lib-${{ github.sha }}
- name: restore cache from es
uses: actions/cache@v3
with:
path: es
key: es-${{ github.sha }}
- name: lessc component
run: npx lessc --js ./es/button/style/index.less
- name: lessc mixins
run: npx lessc --js ./es/style/mixins/index.less
needs: compile
style-dist:
name: dist style compile
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: restore cache from package-lock.json
uses: actions/cache@v3
with:
path: package-temp-dir
key: lock-${{ github.sha }}
- name: restore cache from node_modules
uses: actions/cache@v3
with:
path: node_modules
key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }}
- name: restore cache from lib
uses: actions/cache@v3
with:
path: lib
key: lib-${{ github.sha }}
- name: restore cache from es
uses: actions/cache@v3
with:
path: es
key: es-${{ github.sha }}
- name: restore cache from dist
uses: actions/cache@v3
with:
path: dist
key: dist-${{ github.sha }}
- name: lessc default
run: npx lessc --js ./dist/antd.less
- name: lessc dark
run: npx lessc --js ./dist/antd.dark.less
- name: lessc variable
run: npx lessc --js ./dist/antd.variable.less
needs: [compile, dist]
################################ Test ################################
normal-test:
name: test
@ -358,12 +250,6 @@ jobs:
path: node_modules
key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }}
- name: cache lib
uses: actions/cache@v3
with:
path: lib
key: lib-${{ github.sha }}
- name: cache es
uses: actions/cache@v3
with:
@ -372,9 +258,6 @@ jobs:
- name: compile
run: npm run compile
- name: check
run: node ./tests/dekko/lib.test.js
needs: setup
compiled-module-test:
@ -383,7 +266,7 @@ jobs:
strategy:
matrix:
react: ['16', '17', '18']
module: [lib, es]
module: [es]
shard: ['1/2', '2/2']
env:
REACT: ${{ matrix.react }}

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