diff --git a/.antd-tools.config.js b/.antd-tools.config.js index 7a5bec36739b..5c139e9d30fc 100644 --- a/.antd-tools.config.js +++ b/.antd-tools.config.js @@ -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";`, - ); - // 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`, + 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'), ); - 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, }; diff --git a/.dumi/hooks/useLocale.tsx b/.dumi/hooks/useLocale.tsx new file mode 100644 index 000000000000..6ca4dd51c70b --- /dev/null +++ b/.dumi/hooks/useLocale.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { useLocale as useDumiLocale } from 'dumi'; + +export interface LocaleMap { + cn: Record; + en: Record; +} + +export default function useLocale( + localeMap?: LocaleMap, +): [Record, 'cn' | 'en'] { + const { id } = useDumiLocale(); + const localeType = id === 'zh-CN' ? 'cn' : ('en' as const); + return [localeMap?.[localeType]!, localeType]; +} diff --git a/.dumi/hooks/useLocation.ts b/.dumi/hooks/useLocation.ts new file mode 100644 index 000000000000..3499a3bbe225 --- /dev/null +++ b/.dumi/hooks/useLocation.ts @@ -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, + }; +} diff --git a/.dumi/hooks/useMenu.tsx b/.dumi/hooks/useMenu.tsx new file mode 100644 index 000000000000..a537adac3bda --- /dev/null +++ b/.dumi/hooks/useMenu.tsx @@ -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(() => { + 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>((result, group) => { + if (group.title) { + // 设计文档特殊处理二级分组 + if (pathname.startsWith('/docs/spec')) { + const childrenGroup = group.children.reduce< + Record[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: ( + + {before} + {item.title} + {after} + + ), + 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: ( + + {before} + {item.title} + {after} + + ), + 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: ( + + {before} + {item.title} + + {(item.frontmatter as any).subtitle} + + {after} + + ), + key: item.link.replace(/(-cn$)/g, ''), + })), + }); + } + } else { + result.push( + ...group.children?.map(item => ({ + label: ( + + {before} + {item.title} + {after} + + ), + key: item.link.replace(/(-cn$)/g, ''), + })), + ); + } + return result; + }, []) ?? [] + ); + }, [sidebarData, fullData, pathname]); + + return [menuItems, pathname]; +}; + +export default useMenu; diff --git a/.dumi/hooks/useSiteToken.ts b/.dumi/hooks/useSiteToken.ts new file mode 100644 index 000000000000..425114ea96aa --- /dev/null +++ b/.dumi/hooks/useSiteToken.ts @@ -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 * 2, + codeFamily: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace`, + }, + }; +}; + +export default useSiteToken; diff --git a/.dumi/pages/404/index.tsx b/.dumi/pages/404/index.tsx new file mode 100644 index 000000000000..4f8d37c5260e --- /dev/null +++ b/.dumi/pages/404/index.tsx @@ -0,0 +1,56 @@ +import React, { useEffect } from 'react'; +import { Result, Button } from 'antd'; +import { HomeOutlined } from '@ant-design/icons'; +import { Link, useLocation } from 'dumi'; +import * as utils from '../../theme/utils'; + +export interface NotFoundProps { + router: { + push: (pathname: string) => void; + replace: (pathname: string) => void; + }; +} + +const DIRECT_MAP: Record = { + 'docs/spec/download': 'docs/resources', + 'docs/spec/work-with-us': 'docs/resources', +}; + +const NotFoundPage: React.FC = ({ router }) => { + const { pathname } = useLocation(); + + const isZhCN = utils.isZhCN(pathname); + + useEffect(() => { + const directLinks = Object.keys(DIRECT_MAP); + for (let i = 0; i < directLinks.length; i += 1) { + const matchPath = directLinks[i]; + if (pathname.includes(matchPath)) { + router.replace(utils.getLocalizedPathname(`/${DIRECT_MAP[matchPath]}`, isZhCN).pathname); + } + } + }, []); + + return ( +
+
+ + + + } + /> +
+
+ ); +}; + +export default NotFoundPage; diff --git a/.dumi/pages/index-cn/index.tsx b/.dumi/pages/index-cn/index.tsx new file mode 100644 index 000000000000..4a997ee84395 --- /dev/null +++ b/.dumi/pages/index-cn/index.tsx @@ -0,0 +1 @@ +export { default } from '../index/index'; diff --git a/.dumi/pages/index/components/Banner.tsx b/.dumi/pages/index/components/Banner.tsx new file mode 100644 index 000000000000..672e50865c60 --- /dev/null +++ b/.dumi/pages/index/components/Banner.tsx @@ -0,0 +1,142 @@ +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 designers/developers building beautiful products more flexible and working with happiness', + start: 'Getting Started', + 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 */} +
+
+ + + +
+
+ + {/* Logo */} +
+ {/* Image Bottom Right */} + + + + {/* Image Left Top */} + + {/* Image Left Top */} + + + + Ant Design 5.0 + + +
{locale.slogan}
+
+ + + + + + + + + + + {children} +
+
+ + ); +} diff --git a/.dumi/pages/index/components/BannerRecommends.tsx b/.dumi/pages/index/components/BannerRecommends.tsx new file mode 100644 index 000000000000..3d5ed1c7326b --- /dev/null +++ b/.dumi/pages/index/components/BannerRecommends.tsx @@ -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 ( +
+ {first3.map((extra, index) => { + if (!extra) { + return ; + } + const icon = icons.find((icon) => icon.name === extra.source); + return ( + + {extra.title} + + {extra.description} + +
+ {extra.date} + {icon && } +
+
+ ); + })} +
+ ); +} diff --git a/.dumi/pages/index/components/ComponentsList.tsx b/.dumi/pages/index/components/ComponentsList.tsx new file mode 100644 index 000000000000..004f1fd91817 --- /dev/null +++ b/.dumi/pages/index/components/ComponentsList.tsx @@ -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: ( + + {locale.sampleContent} + + ), + }, + + { + title: 'DatePicker', + type: 'update', + node: ( + + ), + }, + + { + title: 'Progress', + type: 'update', + node: ( + + + + {locale.inProgress} + + + + {locale.success} + + + + {locale.taskFailed} + + + ), + }, + + { + title: 'Tour', + type: 'new', + node: ( + + ), + }, + { + title: 'FloatButton', + type: 'new', + node: ( + + , + }, + { + icon: , + }, + { + icon: , + }, + ]} + /> + + , + }, + { + icon: , + }, + { + icon: , + }, + ]} + /> + + ), + }, + + // { + // title: 'Steps', + // type: 'update', + // node: , + // }, + + { + title: 'Alert', + type: 'update', + node: ( + + ), + }, + ], + [], + ); + + return ( +
+
+ {COMPONENTS.map(({ title, node, type }, index) => { + const tagColor = type === 'new' ? 'processing' : 'warning'; + const tagText = type === 'new' ? locale.new : locale.update; + + return ( +
+ {/* Decorator */} +
+ + {/* Title */} + + + {title} + + {tagText} + + +
+ {node} +
+
+ ); + })} +
+
+ ); +} diff --git a/.dumi/pages/index/components/DesignFramework.tsx b/.dumi/pages/index/components/DesignFramework.tsx new file mode 100644 index 000000000000..d90d870b12c0 --- /dev/null +++ b/.dumi/pages/index/components/DesignFramework.tsx @@ -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 ( + + {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 ( + + +
+ {title} + + + {title} + + + {desc} + +
+ + + ); + })} + + {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 ( + + + {title} + + + {title} + + + {desc} + + + + ); + })} +
+ ); +} diff --git a/.dumi/pages/index/components/Group.tsx b/.dumi/pages/index/components/Group.tsx new file mode 100644 index 000000000000..22f37e06408e --- /dev/null +++ b/.dumi/pages/index/components/Group.tsx @@ -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 ( +
+ {children} +
+ ); +} + +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 = ( + <> +
+ + {title} + + + {description} + +
+ +
+ {children ? ( +
{children}
+ ) : ( +
+ )} +
+ + ); + + return ( +
+
{decoration}
+ + {childNode} + +
+ ); +} diff --git a/.dumi/pages/index/components/RecommendsOld.tsx b/.dumi/pages/index/components/RecommendsOld.tsx new file mode 100644 index 000000000000..6532f541019d --- /dev/null +++ b/.dumi/pages/index/components/RecommendsOld.tsx @@ -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 ( + + {new Array(3).fill(null).map((_, index) => { + const data = recommendations[index]; + + return ( + + {data ? ( +
+
+ {data.title} + {data.description} +
+
+ ) : null} + + ); + })} +
+ ); +} diff --git a/.dumi/pages/index/components/Theme/BackgroundImage.tsx b/.dumi/pages/index/components/Theme/BackgroundImage.tsx new file mode 100644 index 000000000000..bdb6e93ad5cf --- /dev/null +++ b/.dumi/pages/index/components/Theme/BackgroundImage.tsx @@ -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 ( + + ); + })} + + {/*
*/} + + ); +} diff --git a/.dumi/pages/index/components/Theme/ColorPicker.tsx b/.dumi/pages/index/components/Theme/ColorPicker.tsx new file mode 100644 index 000000000000..2d8479f6f063 --- /dev/null +++ b/.dumi/pages/index/components/Theme/ColorPicker.tsx @@ -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 = ({ color, onChange }) => { + const [value, setValue] = useState(color); + + useEffect(() => { + const timeout = setTimeout(() => { + onChange?.(value); + }, 200); + return () => clearTimeout(timeout); + }, [value]); + + useEffect(() => { + setValue(color); + }, [color]); + + return ; +}; + +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 ( + + { + onChange?.(event.target.value); + }} + style={{ width: 120 }} + /> + + + {matchColors.map(({ color, active, picker }) => { + let colorNode = ( +
{ + if (!picker) { + onChange?.(color); + } + }} + /> + ); + + if (picker) { + colorNode = ( + onChange?.(color)} /> + } + trigger="click" + showArrow={false} + > + {colorNode} + + ); + } + + return colorNode; + })} + + + ); +} diff --git a/.dumi/pages/index/components/Theme/RadiusPicker.tsx b/.dumi/pages/index/components/Theme/RadiusPicker.tsx new file mode 100644 index 000000000000..73999de98532 --- /dev/null +++ b/.dumi/pages/index/components/Theme/RadiusPicker.tsx @@ -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 ( + + `${val}px`} + parser={str => (str ? parseFloat(str) : (str as any))} + /> + + + + ); +} diff --git a/.dumi/pages/index/components/Theme/ThemePicker.tsx b/.dumi/pages/index/components/Theme/ThemePicker.tsx new file mode 100644 index 000000000000..9937d2ecac91 --- /dev/null +++ b/.dumi/pages/index/components/Theme/ThemePicker.tsx @@ -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 ( + + {Object.keys(THEMES).map(theme => { + const url = THEMES[theme as THEME]; + + return ( + +
+ { + onChange?.(theme); + }} + /> +
+ {locale[theme as keyof typeof locale]} +
+ ); + })} +
+ ); +} diff --git a/.dumi/pages/index/components/Theme/colorUtil.ts b/.dumi/pages/index/components/Theme/colorUtil.ts new file mode 100644 index 000000000000..042e52ffcc5f --- /dev/null +++ b/.dumi/pages/index/components/Theme/colorUtil.ts @@ -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' + ); +} diff --git a/.dumi/pages/index/components/Theme/index.tsx b/.dumi/pages/index/components/Theme/index.tsx new file mode 100644 index 000000000000..8fab578dd706 --- /dev/null +++ b/.dumi/pages/index/components/Theme/index.tsx @@ -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: , + children: subMenuItems, + }, + { + key: `Development`, + label: `Development`, + icon: , + }, +]; + +// ============================= 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> = { + 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(ThemeDefault); + + const onThemeChange = (_: Partial, 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 = ( + + +
+ +
+ {/* Logo */} +
+
+ +
+

Ant Design 5.0

+
+ + + + +
+ +
+ + + + + + + + + + }>Design + Themes + + + {locale.customizeTheme} + + + + + } + > +
+ + + + + + + + + + + + + {locale.default} + {locale.compact} + + +
+
+
+
+ + +
+
+ ); + + const posStyle: React.CSSProperties = { + position: 'absolute', + }; + + return ( + + {/* >>>>>> Default <<<<<< */} +
+ {/* Image Left Top */} + + {/* Image Right Bottom */} + +
+ + {/* >>>>>> Dark <<<<<< */} +
+ {/* Image Left Top */} + + {/* Image Right Bottom */} + +
+ + {/* >>>>>> Background Image <<<<<< */} + + + } + > + {themeNode} +
+ ); +} diff --git a/site/theme/template/Home/util.tsx b/.dumi/pages/index/components/util.tsx similarity index 100% rename from site/theme/template/Home/util.tsx rename to .dumi/pages/index/components/util.tsx diff --git a/.dumi/pages/index/index.tsx b/.dumi/pages/index/index.tsx new file mode 100644 index 000000000000..0bfa26e06798 --- /dev/null +++ b/.dumi/pages/index/index.tsx @@ -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 ( + +
+ + + + +
+ + + + + + {/* Image Left Top */} + + + } + > + + +
+
+
+ ); +}; + +export default Homepage; diff --git a/.dumi/pages/theme-editor-cn/index.tsx b/.dumi/pages/theme-editor-cn/index.tsx new file mode 100644 index 000000000000..7577ba3a7d91 --- /dev/null +++ b/.dumi/pages/theme-editor-cn/index.tsx @@ -0,0 +1 @@ +export { default } from '../theme-editor/index'; diff --git a/.dumi/pages/theme-editor/index.tsx b/.dumi/pages/theme-editor/index.tsx new file mode 100644 index 000000000000..35d1e25ad2e2 --- /dev/null +++ b/.dumi/pages/theme-editor/index.tsx @@ -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 ( +
+ + { + setTheme(newTheme.config); + }} + /> + +
+ ); +}; + +export default CustomTheme; diff --git a/.dumi/rehypeAntd.ts b/.dumi/rehypeAntd.ts new file mode 100644 index 000000000000..a05398fafc20 --- /dev/null +++ b/.dumi/rehypeAntd.ts @@ -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 { + 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((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; diff --git a/.dumi/theme/antd.js b/.dumi/theme/antd.js new file mode 100644 index 000000000000..b35b5b28907c --- /dev/null +++ b/.dumi/theme/antd.js @@ -0,0 +1,4 @@ +// Need import for the additional core style +// exports.styleCore = require('../components/style/reset.css'); + +module.exports = require('../../components'); diff --git a/.dumi/theme/builtins/APITable/index.tsx b/.dumi/theme/builtins/APITable/index.tsx new file mode 100644 index 000000000000..aad36b22ccf8 --- /dev/null +++ b/.dumi/theme/builtins/APITable/index.tsx @@ -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; diff --git a/.dumi/theme/builtins/Alert/index.tsx b/.dumi/theme/builtins/Alert/index.tsx new file mode 100644 index 000000000000..60fe59f45e78 --- /dev/null +++ b/.dumi/theme/builtins/Alert/index.tsx @@ -0,0 +1,8 @@ +import { Alert, AlertProps } from 'antd'; +import React, { FC } from 'react'; + +const MdAlert: FC = ({ style, ...props }) => { + return ; +}; + +export default MdAlert; diff --git a/.dumi/theme/builtins/ColorPaletteTool/index.tsx b/.dumi/theme/builtins/ColorPaletteTool/index.tsx new file mode 100644 index 000000000000..ed5147e4ecd6 --- /dev/null +++ b/.dumi/theme/builtins/ColorPaletteTool/index.tsx @@ -0,0 +1,4 @@ +// @ts-ignore +import ColorPaletteTool from '../../common/Color/ColorPaletteTool'; + +export default ColorPaletteTool; diff --git a/.dumi/theme/builtins/ColorPaletteToolDark/index.tsx b/.dumi/theme/builtins/ColorPaletteToolDark/index.tsx new file mode 100644 index 000000000000..f47d9e1add41 --- /dev/null +++ b/.dumi/theme/builtins/ColorPaletteToolDark/index.tsx @@ -0,0 +1,4 @@ +// @ts-ignore +import ColorPaletteToolDark from '../../common/Color/ColorPaletteToolDark'; + +export default ColorPaletteToolDark; diff --git a/.dumi/theme/builtins/ColorPalettes/index.tsx b/.dumi/theme/builtins/ColorPalettes/index.tsx new file mode 100644 index 000000000000..cfb9e58bfb3e --- /dev/null +++ b/.dumi/theme/builtins/ColorPalettes/index.tsx @@ -0,0 +1,4 @@ +// @ts-ignore +import ColorPalettes from '../../common/Color/ColorPalettes'; + +export default ColorPalettes; diff --git a/.dumi/theme/builtins/ComponentOverview/ProComponentsList.ts b/.dumi/theme/builtins/ComponentOverview/ProComponentsList.ts new file mode 100644 index 000000000000..3b1c78675243 --- /dev/null +++ b/.dumi/theme/builtins/ComponentOverview/ProComponentsList.ts @@ -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; diff --git a/.dumi/theme/builtins/ComponentOverview/index.tsx b/.dumi/theme/builtins/ComponentOverview/index.tsx new file mode 100644 index 000000000000..f20464eecd73 --- /dev/null +++ b/.dumi/theme/builtins/ComponentOverview/index.tsx @@ -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(''); + + const sectionRef = React.useRef(null); + + const onKeyDown: React.KeyboardEventHandler = event => { + if (event.keyCode === 13 && search.trim().length) { + sectionRef.current?.querySelector('.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 ( +
+ + { + setSearch(e.target.value); + reportSearch(e.target.value); + }} + onKeyDown={onKeyDown} + autoFocus // eslint-disable-line jsx-a11y/no-autofocus + suffix={} + /> + + {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 ? ( +
+ + <Space align="center"> + <span style={{ fontSize: 24 }}>{group.title}</span> + <Tag style={{ display: 'block' }}>{components.length}</Tag> + </Space> + + + {components.map(component => { + const url = `${component.link}/`; + + /** Link 不能跳转到外链 */ + const ComponentLink = !url.startsWith('http') ? Link : 'a'; + + return ( + + onClickCard(url)}> + + {component.title} {component.subtitle} +
+ } + > +
+ {component.title} +
+ + + + ); + })} + +
+ ) : null; + })} + + ); +}; + +export default memo(Overview); diff --git a/.dumi/theme/builtins/DemoWrapper/index.tsx b/.dumi/theme/builtins/DemoWrapper/index.tsx new file mode 100644 index 000000000000..9cdb74256f7e --- /dev/null +++ b/.dumi/theme/builtins/DemoWrapper/index.tsx @@ -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 ( +
+ + + } + > + {expandAll ? ( + + ) : ( + + )} + + + } + > + {showDebug ? ( + + ) : ( + + )} + + + {/* FIXME: find a new way instead of `key` to trigger re-render */} + +
+ ); +}; + +export default DemoWrapper; diff --git a/site/theme/template/IconDisplay/Category.tsx b/.dumi/theme/builtins/IconSearch/Category.tsx similarity index 85% rename from site/theme/template/IconDisplay/Category.tsx rename to .dumi/theme/builtins/IconSearch/Category.tsx index 792f2032f487..06813736a712 100644 --- a/site/theme/template/IconDisplay/Category.tsx +++ b/.dumi/theme/builtins/IconSearch/Category.tsx @@ -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 = props => { - const { icons, title, newIcons, theme, intl } = props; + const { icons, title, newIcons, theme } = props; + const intl = useIntl(); const [justCopied, setJustCopied] = React.useState(null); const copyId = React.useRef(null); const onCopied = React.useCallback((type: string, text: string) => { @@ -38,7 +38,7 @@ const Category: React.FC = props => { ); return (
-

{intl.messages[`app.docs.components.icon.category.${title}`]}

+

{intl.formatMessage({ id: `app.docs.components.icon.category.${title}` })}

    {icons.map(name => ( = props => { ); }; -export default injectIntl(Category); +export default Category; diff --git a/site/theme/template/IconDisplay/CopyableIcon.tsx b/.dumi/theme/builtins/IconSearch/CopyableIcon.tsx similarity index 100% rename from site/theme/template/IconDisplay/CopyableIcon.tsx rename to .dumi/theme/builtins/IconSearch/CopyableIcon.tsx diff --git a/site/theme/template/IconDisplay/IconPicSearcher.tsx b/.dumi/theme/builtins/IconSearch/IconPicSearcher.tsx similarity index 83% rename from site/theme/template/IconDisplay/IconPicSearcher.tsx rename to .dumi/theme/builtins/IconSearch/IconPicSearcher.tsx index 2c40e9ed2c14..7958d79b282b 100644 --- a/site/theme/template/IconDisplay/IconPicSearcher.tsx +++ b/.dumi/theme/builtins/IconSearch/IconPicSearcher.tsx @@ -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 = ({ intl }) => { - const { messages } = intl; +const PicSearcher: React.FC = () => { + const intl = useIntl(); const [state, setState] = useState({ loading: false, modalOpen: false, @@ -63,7 +59,7 @@ const PicSearcher: React.FC = ({ intl }) => { } }; // eslint-disable-next-line class-methods-use-this - const toImage = (url: string) => + const toImage = (url: string): Promise => new Promise(resolve => { const img = new Image(); img.setAttribute('crossOrigin', 'anonymous'); @@ -139,13 +135,13 @@ const PicSearcher: React.FC = ({ intl }) => { return (
    = ({ intl }) => { {state.modelLoaded || (
    @@ -170,21 +166,21 @@ const PicSearcher: React.FC = ({ intl }) => {

    - {messages['app.docs.components.icon.pic-searcher.upload-text']} + {intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-text' })}

    - {messages['app.docs.components.icon.pic-searcher.upload-hint']} + {intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-hint' })}

    )}
    {state.icons.length > 0 && (
    - {messages['app.docs.components.icon.pic-searcher.result-tip']} + {intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.result-tip' })}
    )} @@ -192,9 +188,11 @@ const PicSearcher: React.FC = ({ intl }) => { + - )} @@ -226,7 +224,9 @@ const PicSearcher: React.FC = ({ intl }) => { )} @@ -236,4 +236,4 @@ const PicSearcher: React.FC = ({ intl }) => { ); }; -export default injectIntl(PicSearcher); +export default PicSearcher; diff --git a/site/theme/template/IconDisplay/fields.ts b/.dumi/theme/builtins/IconSearch/fields.ts similarity index 99% rename from site/theme/template/IconDisplay/fields.ts rename to .dumi/theme/builtins/IconSearch/fields.ts index de37e675cf86..2e0f954bfed1 100644 --- a/site/theme/template/IconDisplay/fields.ts +++ b/.dumi/theme/builtins/IconSearch/fields.ts @@ -200,8 +200,6 @@ const logo = [ 'Yahoo', 'Reddit', 'Sketch', - 'WhatsApp', - 'Dingtalk', ]; const datum = [...direction, ...suggestion, ...editor, ...data, ...logo]; diff --git a/.dumi/theme/builtins/IconSearch/index.tsx b/.dumi/theme/builtins/IconSearch/index.tsx new file mode 100644 index 000000000000..c2424ab58e49 --- /dev/null +++ b/.dumi/theme/builtins/IconSearch/index.tsx @@ -0,0 +1,118 @@ +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 { useIntl } from 'dumi'; +import debounce from 'lodash/debounce'; +import Category from './Category'; +import IconPicSearcher from './IconPicSearcher'; +import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons'; +import type { CategoriesKeys } from './fields'; +import { categories } from './fields'; + +export enum ThemeType { + Filled = 'Filled', + Outlined = 'Outlined', + TwoTone = 'TwoTone', +} + +const allIcons: { [key: string]: any } = AntdIcons; + +interface IconSearchState { + theme: ThemeType; + searchKey: string; +} + +const IconSearch: React.FC = () => { + const intl = useIntl(); + const [displayState, setDisplayState] = React.useState({ + theme: ThemeType.Outlined, + searchKey: '', + }); + + const newIconNames: string[] = []; + + const handleSearchIcon = React.useCallback( + debounce((searchKey: string) => { + setDisplayState(prevState => ({ ...prevState, searchKey })); + }), + [], + ); + + const handleChangeTheme = React.useCallback((e: RadioChangeEvent) => { + setDisplayState(prevState => ({ ...prevState, theme: e.target.value as ThemeType })); + }, []); + + const renderCategories = React.useMemo(() => { + const { searchKey = '', theme } = displayState; + + const categoriesResult = Object.keys(categories) + .map(key => { + let iconList = categories[key as CategoriesKeys]; + if (searchKey) { + const matchKey = searchKey + // eslint-disable-next-line prefer-regex-literals + .replace(new RegExp(`^<([a-zA-Z]*)\\s/>$`, 'gi'), (_, name) => name) + .replace(/(Filled|Outlined|TwoTone)$/, '') + .toLowerCase(); + iconList = iconList.filter(iconName => iconName.toLowerCase().includes(matchKey)); + } + + // CopyrightCircle is same as Copyright, don't show it + iconList = iconList.filter(icon => icon !== 'CopyrightCircle'); + + return { + category: key, + icons: iconList.map(iconName => iconName + theme).filter(iconName => allIcons[iconName]), + }; + }) + .filter(({ icons }) => !!icons.length) + .map(({ category, icons }) => ( + + )); + return categoriesResult.length === 0 ? : categoriesResult; + }, [displayState.searchKey, displayState.theme]); + return ( +
    +
    + + + {' '} + {intl.formatMessage({ id: 'app.docs.components.icon.outlined' })} + + + {' '} + {intl.formatMessage({ id: 'app.docs.components.icon.filled' })} + + + {' '} + {intl.formatMessage({ id: 'app.docs.components.icon.two-tone' })} + + + handleSearchIcon(e.currentTarget.value)} + size="large" + autoFocus + suffix={} + /> +
    + {renderCategories} +
    + ); +}; + +export default IconSearch; diff --git a/.dumi/theme/builtins/IconSearch/themeIcons.tsx b/.dumi/theme/builtins/IconSearch/themeIcons.tsx new file mode 100644 index 000000000000..eeb4f8782857 --- /dev/null +++ b/.dumi/theme/builtins/IconSearch/themeIcons.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import type { CustomIconComponentProps } from '@ant-design/icons/es/components/Icon'; + +type CustomIconComponent = React.ComponentType< + CustomIconComponentProps | React.SVGProps +>; + +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' + + '0c0-53-43-96-96-96z'; + return ( + + + + ); +}; + +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' + + '0-53-43-96-96-96z m-12 800H172c-6.6 0-12-5.4-' + + '12-12V172c0-6.6 5.4-12 12-12h680c6.6 0 12 5.4' + + ' 12 12v680c0 6.6-5.4 12-12 12z'; + return ( + + + + ); +}; + +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.' + + '066 16 512z m496 368V144c203.41 0 368 164.622 3' + + '68 368 0 203.41-164.622 368-368 368z'; + return ( + + + + ); +}; diff --git a/.dumi/theme/builtins/ImagePreview/index.jsx b/.dumi/theme/builtins/ImagePreview/index.jsx new file mode 100644 index 000000000000..331740e73739 --- /dev/null +++ b/.dumi/theme/builtins/ImagePreview/index.jsx @@ -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 ( +
    +
    + {coverMeta.alt} +
    +
    {coverMeta.alt}
    +
    + + + {comparable ? cover : imgs} + +
    {coverMeta.alt}
    +
    +
    + ); +} + +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 ( +
    +
    + {meta.alt} +
    +
    + ); + }); + + 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 ( +
    + {imagesList.map((_, index) => { + if (!comparable && index !== 0) { + return null; + } + + return ( + { + this.handleClick(index); + }} + onCancel={this.handleCancel} + /> + ); + })} +
    + ); + } +} diff --git a/.dumi/theme/builtins/Palette/index.tsx b/.dumi/theme/builtins/Palette/index.tsx new file mode 100644 index 000000000000..b94369dd0b0a --- /dev/null +++ b/.dumi/theme/builtins/Palette/index.tsx @@ -0,0 +1,4 @@ +// @ts-ignore +import Palette from '../../common/Color/Palette'; + +export default Palette; diff --git a/.dumi/theme/builtins/Previewer/fromDumiProps.tsx b/.dumi/theme/builtins/Previewer/fromDumiProps.tsx new file mode 100644 index 000000000000..4f4cba3a8989 --- /dev/null +++ b/.dumi/theme/builtins/Previewer/fromDumiProps.tsx @@ -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; + +function useShowRiddleButton() { + const [showRiddleButton, setShowRiddleButton] = useState(false); + + useEffect(() => { + pingDeferrer ??= new Promise(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

    ( + WrappedComponent: React.ComponentType

    , +): FC { + 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 ; + }; + + return hoc; +} diff --git a/.dumi/theme/builtins/Previewer/index.jsx b/.dumi/theme/builtins/Previewer/index.jsx new file mode 100644 index 000000000000..b0fa3456ef77 --- /dev/null +++ b/.dumi/theme/builtins/Previewer/index.jsx @@ -0,0 +1,523 @@ +/* eslint jsx-a11y/no-noninteractive-element-interactions: 0 */ +import { CheckOutlined, SnippetsOutlined, ThunderboltOutlined } from '@ant-design/icons'; +import stackblitzSdk from '@stackblitz/sdk'; +import { Alert, Badge, Tooltip } from 'antd'; +import classNames from 'classnames'; +import LZString from 'lz-string'; +import React from 'react'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import ReactDOM from 'react-dom'; +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; + +function compress(string) { + return LZString.compressToBase64(string) + .replace(/\+/g, '-') // Convert '+' to '-' + .replace(/\//g, '_') // Convert '/' to '_' + .replace(/=+$/, ''); // Remove ending '=' +} + +class Demo extends React.Component { + iframeRef = React.createRef(); + + codeSandboxIconRef = React.createRef(); + + riddleIconRef = React.createRef(); + + codepenIconRef = React.createRef(); + + state = { + codeExpand: false, + copied: false, + copyTooltipOpen: false, + codeType: 'tsx', + }; + + componentDidMount() { + const { meta, location } = this.props; + if (meta.id === location.hash.slice(1)) { + this.anchor.click(); + } + } + + shouldComponentUpdate(nextProps, nextState) { + const { codeExpand, copied, copyTooltipOpen, codeType } = this.state; + const { expand, theme, showRiddleButton } = this.props; + return ( + (codeExpand || expand) !== (nextState.codeExpand || nextProps.expand) || + copied !== nextState.copied || + copyTooltipOpen !== nextState.copyTooltipOpen || + codeType !== nextState.copyTooltipOpen || + nextProps.theme !== theme || + nextProps.showRiddleButton !== showRiddleButton + ); + } + + getSourceCode() { + const { highlightedCodes } = this.props; + const { codeType } = this.state; + if (typeof document !== 'undefined') { + const div = document.createElement('div'); + const divJSX = document.createElement('div'); + div.innerHTML = highlightedCodes[codeType] || highlightedCodes.jsx; + divJSX.innerHTML = highlightedCodes.jsx; + return [divJSX.textContent, div.textContent]; + } + return ['', '']; + } + + handleCodeExpand = demo => { + const { codeExpand } = this.state; + this.setState({ codeExpand: !codeExpand }); + this.track({ + type: 'expand', + demo, + }); + }; + + saveAnchor = anchor => { + this.anchor = anchor; + }; + + handleCodeCopied = demo => { + this.setState({ copied: true }); + this.track({ + type: 'copy', + demo, + }); + }; + + onCopyTooltipOpenChange = open => { + if (open) { + this.setState({ + copyTooltipOpen: open, + copied: false, + }); + return; + } + this.setState({ + copyTooltipOpen: open, + }); + }; + + // eslint-disable-next-line class-methods-use-this + track({ type, demo }) { + if (!window.gtag) { + return; + } + window.gtag('event', 'demo', { + event_category: type, + event_label: demo, + }); + } + + handleIframeReady = () => { + const { theme, setIframeTheme } = this.props; + if (this.iframeRef.current) { + // setIframeTheme(this.iframeRef.current, theme); + } + }; + + render() { + const { state } = this; + const { props } = this; + const { + meta, + src, + content, + preview, + highlightedCodes, + style, + highlightedStyle, + expand, + intl: { locale }, + theme, + showRiddleButton, + } = props; + const { copied, copyTooltipOpen, codeType } = state; + if (!this.liveDemo) { + this.liveDemo = meta.iframe ? ( + +

    - {messages['app.docs.components.icon.pic-searcher.th-icon']} + {intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-icon' })} + + {intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-score' })} {messages['app.docs.components.icon.pic-searcher.th-score']}