mirror of
https://github.com/ant-design/ant-design.git
synced 2026-02-09 10:59:19 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b092c022aa | ||
|
|
5995aef78a |
@@ -1,40 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const restCssPath = path.join(process.cwd(), 'components', 'style', 'reset.css');
|
||||
const antdCssPath = path.join(process.cwd(), 'components', 'style', 'antd.css');
|
||||
const tokenStatisticPath = path.join(process.cwd(), 'components', 'version', 'token.json');
|
||||
const tokenMetaPath = path.join(process.cwd(), 'components', 'version', 'token-meta.json');
|
||||
|
||||
function finalizeCompile() {
|
||||
if (fs.existsSync(path.join(__dirname, './es'))) {
|
||||
fs.copyFileSync(restCssPath, path.join(process.cwd(), 'es', 'style', 'reset.css'));
|
||||
fs.copyFileSync(antdCssPath, path.join(process.cwd(), 'es', 'style', 'antd.css'));
|
||||
fs.copyFileSync(tokenStatisticPath, path.join(process.cwd(), 'es', 'version', 'token.json'));
|
||||
fs.copyFileSync(tokenMetaPath, path.join(process.cwd(), 'es', 'version', 'token-meta.json'));
|
||||
}
|
||||
|
||||
if (fs.existsSync(path.join(__dirname, './lib'))) {
|
||||
fs.copyFileSync(restCssPath, path.join(process.cwd(), 'lib', 'style', 'reset.css'));
|
||||
fs.copyFileSync(antdCssPath, path.join(process.cwd(), 'lib', 'style', 'antd.css'));
|
||||
fs.copyFileSync(tokenStatisticPath, path.join(process.cwd(), 'lib', 'version', 'token.json'));
|
||||
fs.copyFileSync(tokenMetaPath, path.join(process.cwd(), 'lib', 'version', 'token-meta.json'));
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeDist() {
|
||||
if (fs.existsSync(path.join(__dirname, './dist'))) {
|
||||
fs.copyFileSync(restCssPath, path.join(process.cwd(), 'dist', 'reset.css'));
|
||||
fs.copyFileSync(antdCssPath, path.join(process.cwd(), 'dist', 'antd.css'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compile: {
|
||||
finalize: finalizeCompile,
|
||||
},
|
||||
dist: {
|
||||
finalize: finalizeDist,
|
||||
},
|
||||
bail: true,
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
module.exports = {
|
||||
ignore: [
|
||||
'**/~*/**',
|
||||
'**/_*/**',
|
||||
'**/icon/**',
|
||||
'**/__tests__/**',
|
||||
'**/style/**',
|
||||
'**/locale/**',
|
||||
'**/*-provider/**',
|
||||
'**/*.json',
|
||||
],
|
||||
modulePattern: [
|
||||
{
|
||||
pattern: /ConfigContext.*renderEmpty/s,
|
||||
module: '../empty',
|
||||
},
|
||||
{
|
||||
pattern: /ConfigConsumer.*renderEmpty/s,
|
||||
module: '../empty',
|
||||
},
|
||||
{
|
||||
pattern: /config-provider\/context.*renderEmpty/s,
|
||||
module: '../empty',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||
{
|
||||
"name": "ant-design",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:4-22-bookworm",
|
||||
"postCreateCommand": "pnpm install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"shezhangzhang.antd-design-token",
|
||||
"fi3ework.vscode-antd-rush"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
node_modules/
|
||||
@@ -1,142 +0,0 @@
|
||||
// FIXME: workaround for avoid searchbar styles be extracted to async chunk
|
||||
@import 'dumi/theme-default/slots/SearchBar/index.less';
|
||||
|
||||
.demo-logo {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
height: 32px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
margin-inline-end: 24px;
|
||||
}
|
||||
|
||||
.demo-logo-vertical {
|
||||
height: 32px;
|
||||
margin: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #eaeaea transparent;
|
||||
@supports (text-autospace: normal) {
|
||||
text-autospace: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.rc-footer {
|
||||
position: relative;
|
||||
clear: both;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
background-color: #000;
|
||||
}
|
||||
.rc-footer a {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-decoration: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.rc-footer a:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
.rc-footer-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
padding: 80px 0 20px;
|
||||
}
|
||||
.rc-footer-columns {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.rc-footer-column {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.rc-footer-column h2 {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
.rc-footer-column-icon {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
margin-inline-end: 0.5em;
|
||||
}
|
||||
.rc-footer-column-icon > span,
|
||||
.rc-footer-column-icon > svg,
|
||||
.rc-footer-column-icon img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.rc-footer-item {
|
||||
margin: 12px 0;
|
||||
}
|
||||
.rc-footer-item-icon {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
margin-inline-end: 0.4em;
|
||||
}
|
||||
.rc-footer-item-icon > span,
|
||||
.rc-footer-item-icon > svg,
|
||||
.rc-footer-item-icon img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.rc-footer-item-separator {
|
||||
margin: 0 0.3em;
|
||||
}
|
||||
.rc-footer-bottom-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 0;
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
.rc-footer-light {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
background-color: transparent;
|
||||
}
|
||||
.rc-footer-light h2,
|
||||
.rc-footer-light a {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
.rc-footer-light .rc-footer-bottom-container {
|
||||
border-top-color: #e8e8e8;
|
||||
}
|
||||
.rc-footer-light .rc-footer-item-separator,
|
||||
.rc-footer-light .rc-footer-item-description {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
@media only screen and (max-width: 767.99px) {
|
||||
.rc-footer {
|
||||
text-align: center;
|
||||
}
|
||||
.rc-footer-container {
|
||||
padding: 40px 0;
|
||||
}
|
||||
.rc-footer-columns {
|
||||
display: block;
|
||||
}
|
||||
.rc-footer-column {
|
||||
display: block;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.rc-footer-column:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export const DarkContext = React.createContext(false);
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
|
||||
const isNumber = (value: any): value is number => {
|
||||
return typeof value === 'number' && !Number.isNaN(value);
|
||||
};
|
||||
|
||||
const fetcher = async (url: string): Promise<number> => {
|
||||
const res = await fetch(url, { headers: { Accept: 'application/vnd.github+json' } });
|
||||
const data = await res.json();
|
||||
const totalCount = isNumber(data?.total_count) ? data.total_count : 0;
|
||||
return totalCount;
|
||||
};
|
||||
|
||||
const swrConfig: SWRConfiguration<number, Error> = {
|
||||
revalidateOnReconnect: true, // 网络重新连接时重新请求
|
||||
dedupingInterval: 1000 * 60, // 1 分钟内重复 key 不会重新请求
|
||||
shouldRetryOnError: true, // 错误重试
|
||||
errorRetryCount: 3, // 最多重试 3 次
|
||||
};
|
||||
|
||||
export interface UseIssueCountOptions {
|
||||
repo: string; // e.g. ant-design/ant-design
|
||||
proxyEndpoint?: string; // backend proxy endpoint to avoid GitHub rate limit
|
||||
titleKeywords?: string[]; // keywords to match in issue title
|
||||
}
|
||||
|
||||
export const useIssueCount = (options: UseIssueCountOptions) => {
|
||||
const { repo, proxyEndpoint, titleKeywords } = options;
|
||||
|
||||
// Note: current query only filters by title keywords. Filtering by component name can be added later if needed.
|
||||
const searchUrl = useMemo(() => {
|
||||
const tokens = (titleKeywords || []).filter(Boolean).map((k) => encodeURIComponent(String(k)));
|
||||
const orExpr = tokens.length > 0 ? tokens.join('%20OR%20') : '';
|
||||
const titlePart = orExpr ? `in:title+(${orExpr})` : 'in:title';
|
||||
const q = `repo:${repo}+is:issue+is:open+${titlePart}`;
|
||||
return `https://api.github.com/search/issues?q=${q}`;
|
||||
}, [repo, titleKeywords]);
|
||||
|
||||
const endpoint = proxyEndpoint || searchUrl;
|
||||
|
||||
const { data, error, isLoading } = useSWR<number, Error>(endpoint || null, fetcher, swrConfig);
|
||||
|
||||
const issueNewUrl = `https://github.com/${repo}/issues/new/choose`;
|
||||
|
||||
const issueSearchUrl = useMemo(() => {
|
||||
const keywords = (titleKeywords || []).filter(Boolean).map((k) => String(k));
|
||||
const groupExpr =
|
||||
keywords.length > 0 ? `(${keywords.map((k) => `is:issue in:title ${k}`).join(' OR ')})` : '';
|
||||
const qRaw = `is:open ${groupExpr}`.trim();
|
||||
return `https://github.com/${repo}/issues?q=${encodeURIComponent(qRaw)}`;
|
||||
}, [repo, titleKeywords]);
|
||||
|
||||
return {
|
||||
issueCount: data,
|
||||
issueCountError: error,
|
||||
issueCountLoading: isLoading,
|
||||
issueNewUrl,
|
||||
issueSearchUrl,
|
||||
};
|
||||
};
|
||||
|
||||
export default useIssueCount;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { startTransition, useState } from 'react';
|
||||
|
||||
const useLayoutState: typeof useState = <S>(
|
||||
...args: Parameters<typeof useState<S>>
|
||||
): ReturnType<typeof useState<S>> => {
|
||||
const [state, setState] = useState<S>(...args);
|
||||
|
||||
const setLayoutState: typeof setState = (...setStateArgs) => {
|
||||
startTransition(() => {
|
||||
setState(...setStateArgs);
|
||||
});
|
||||
};
|
||||
|
||||
return [state, setLayoutState];
|
||||
};
|
||||
|
||||
export default useLayoutState;
|
||||
@@ -1,113 +0,0 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
const ANT_SYNC_STORAGE_EVENT_KEY = 'ANT_SYNC_STORAGE_EVENT_KEY';
|
||||
|
||||
const isFunction = (val: any): val is (...args: any[]) => any => {
|
||||
return typeof val === 'function';
|
||||
};
|
||||
|
||||
interface Options<T> {
|
||||
defaultValue?: T;
|
||||
serializer?: (value: T) => string;
|
||||
deserializer?: (value: string) => T;
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
const useLocalStorage = <T>(key: string, options: Options<T> = {}) => {
|
||||
const storage = typeof window !== 'undefined' ? localStorage : null;
|
||||
|
||||
const { serializer, deserializer, onError, defaultValue } = options;
|
||||
|
||||
const mergedSerializer = typeof serializer === 'function' ? serializer : JSON.stringify;
|
||||
|
||||
const mergedDeserializer = typeof deserializer === 'function' ? deserializer : JSON.parse;
|
||||
|
||||
const handleError = typeof onError === 'function' ? onError : console.error;
|
||||
|
||||
const getStoredValue = () => {
|
||||
try {
|
||||
const rawData = storage?.getItem(key);
|
||||
if (rawData) {
|
||||
return mergedDeserializer(rawData);
|
||||
}
|
||||
} catch (e) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
handleError(e);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
const [state, setState] = React.useState<T>(getStoredValue);
|
||||
|
||||
useEffect(() => {
|
||||
setState(getStoredValue());
|
||||
}, [key]);
|
||||
|
||||
const updateState: React.Dispatch<React.SetStateAction<T>> = (value) => {
|
||||
const currentState = isFunction(value) ? value(state) : value;
|
||||
setState(currentState);
|
||||
try {
|
||||
let newValue: string | null;
|
||||
const oldValue = storage?.getItem(key);
|
||||
if (typeof currentState === 'undefined') {
|
||||
newValue = null;
|
||||
storage?.removeItem(key);
|
||||
} else {
|
||||
newValue = mergedSerializer(currentState);
|
||||
storage?.setItem(key, newValue);
|
||||
}
|
||||
dispatchEvent(
|
||||
new CustomEvent(ANT_SYNC_STORAGE_EVENT_KEY, {
|
||||
detail: { key, newValue, oldValue, storageArea: storage },
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const shouldSync = (ev: StorageEvent) => {
|
||||
return ev && ev.key === key && ev.storageArea === storage;
|
||||
};
|
||||
|
||||
const onNativeStorage = useCallback(
|
||||
(event: StorageEvent) => {
|
||||
if (shouldSync(event)) {
|
||||
setState(getStoredValue());
|
||||
}
|
||||
},
|
||||
[key],
|
||||
);
|
||||
|
||||
const shouldSyncCustomEvent = (ev: CustomEvent<{ key: string; storageArea: Storage }>) => {
|
||||
return ev?.detail?.key === key && ev?.detail?.storageArea === storage;
|
||||
};
|
||||
|
||||
const onCustomStorage = useCallback(
|
||||
(event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
|
||||
if (shouldSyncCustomEvent(customEvent)) {
|
||||
setState(getStoredValue());
|
||||
}
|
||||
},
|
||||
[key],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window?.addEventListener('storage', onNativeStorage);
|
||||
window?.addEventListener(ANT_SYNC_STORAGE_EVENT_KEY, onCustomStorage);
|
||||
return () => {
|
||||
window?.removeEventListener('storage', onNativeStorage);
|
||||
window?.removeEventListener(ANT_SYNC_STORAGE_EVENT_KEY, onCustomStorage);
|
||||
};
|
||||
}, [key, onNativeStorage, onCustomStorage]);
|
||||
|
||||
return [state, updateState] as const;
|
||||
};
|
||||
|
||||
export default useLocalStorage;
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useLocale as useDumiLocale } from 'dumi';
|
||||
|
||||
export interface LocaleMap<
|
||||
K extends PropertyKey = PropertyKey,
|
||||
V extends string | ((...params: any[]) => string) = string,
|
||||
> {
|
||||
cn: Record<K, V>;
|
||||
en: Record<K, V>;
|
||||
}
|
||||
|
||||
const useLocale = <
|
||||
K extends PropertyKey = PropertyKey,
|
||||
V extends string | ((...params: any[]) => string) = string,
|
||||
>(
|
||||
localeMap?: LocaleMap<K, V>,
|
||||
): [Record<K, V>, 'cn' | 'en'] => {
|
||||
const { id } = useDumiLocale();
|
||||
const localeType = id === 'zh-CN' ? 'cn' : 'en';
|
||||
return [localeMap?.[localeType] ?? ({} as Record<K, V>), localeType] as const;
|
||||
};
|
||||
|
||||
export default useLocale;
|
||||
@@ -1,48 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useLocation as useDumiLocation } from 'dumi';
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Flex, Tag, version } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
import { useFullSidebarData, useSidebarData } from 'dumi';
|
||||
|
||||
import Link from '../theme/common/Link';
|
||||
import useLocale from './useLocale';
|
||||
import useLocation from './useLocation';
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
deprecated: '废弃',
|
||||
updated: '更新',
|
||||
new: '新增',
|
||||
},
|
||||
en: {
|
||||
deprecated: 'DEPRECATED',
|
||||
updated: 'UPDATED',
|
||||
new: 'NEW',
|
||||
},
|
||||
};
|
||||
|
||||
const getTagColor = (val?: string) => {
|
||||
switch (val?.toUpperCase()) {
|
||||
case 'UPDATED':
|
||||
return 'processing';
|
||||
case 'DEPRECATED':
|
||||
return 'red';
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
};
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
link: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`,
|
||||
tag: css`
|
||||
margin-inline-end: 0;
|
||||
`,
|
||||
subtitle: css`
|
||||
font-weight: normal;
|
||||
font-size: ${cssVar.fontSizeSM};
|
||||
opacity: 0.8;
|
||||
margin-inline-start: ${cssVar.marginSM};
|
||||
`,
|
||||
}));
|
||||
|
||||
interface MenuItemLabelProps {
|
||||
before?: React.ReactNode;
|
||||
after?: React.ReactNode;
|
||||
link: string;
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
search?: string;
|
||||
tag?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MenuItemLabelWithTag: React.FC<MenuItemLabelProps> = (props) => {
|
||||
const { before, after, link, title, subtitle, search, tag, className } = props;
|
||||
|
||||
const [locale] = useLocale(locales);
|
||||
|
||||
const getLocale = (name: string) => {
|
||||
return (locale as any)[name.toLowerCase()] ?? name;
|
||||
};
|
||||
|
||||
if (!before && !after) {
|
||||
return (
|
||||
<Link to={`${link}${search}`} className={clsx(className, { [styles.link]: tag })}>
|
||||
<Flex justify="flex-start" align="center">
|
||||
<span>{title}</span>
|
||||
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
||||
</Flex>
|
||||
{tag && (
|
||||
<Tag variant="filled" className={clsx(styles.tag)} color={getTagColor(tag)}>
|
||||
{getLocale(tag.replace(/VERSION/i, version))}
|
||||
</Tag>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link to={`${link}${search}`} className={className}>
|
||||
{before}
|
||||
{title}
|
||||
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
||||
{after}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export interface UseMenuOptions {
|
||||
before?: React.ReactNode;
|
||||
after?: React.ReactNode;
|
||||
}
|
||||
|
||||
const useMenu = (options: UseMenuOptions = {}): readonly [MenuProps['items'], string] => {
|
||||
const fullData = useFullSidebarData();
|
||||
const { pathname, search } = 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.splice(1, 0, changelogData[0]);
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith('/changelog')) {
|
||||
const reactDocData = Object.entries(fullData).find(([key]) =>
|
||||
key.startsWith('/docs/react'),
|
||||
)?.[1];
|
||||
if (reactDocData) {
|
||||
sidebarItems.unshift(reactDocData[0]);
|
||||
sidebarItems.push(...reactDocData.slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
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?.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}${search}`}>
|
||||
{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}${search}`}>
|
||||
{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: (
|
||||
<MenuItemLabelWithTag
|
||||
before={before}
|
||||
after={after}
|
||||
link={item.link}
|
||||
title={item?.title}
|
||||
subtitle={item.frontmatter?.subtitle}
|
||||
search={search}
|
||||
tag={item.frontmatter?.tag}
|
||||
/>
|
||||
),
|
||||
key: item.link.replace(/(-cn$)/g, ''),
|
||||
})),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const list = group.children || [];
|
||||
// 如果有 date 字段,我们就对其进行排序
|
||||
if (list.every((info) => info?.frontmatter?.date)) {
|
||||
list.sort((a, b) => (a.frontmatter?.date > b.frontmatter?.date ? -1 : 1));
|
||||
}
|
||||
result.push(
|
||||
...list.map((item) => ({
|
||||
label: (
|
||||
<MenuItemLabelWithTag
|
||||
before={before}
|
||||
after={after}
|
||||
link={item.link}
|
||||
title={item?.title}
|
||||
search={search}
|
||||
tag={item.frontmatter?.tag}
|
||||
/>
|
||||
),
|
||||
key: item.link.replace(/(-cn$)/g, ''),
|
||||
})),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, []) ?? []
|
||||
);
|
||||
}, [sidebarData, pathname, fullData, search, before, after]);
|
||||
|
||||
return [menuItems, pathname] as const;
|
||||
};
|
||||
|
||||
export default useMenu;
|
||||
@@ -1,124 +0,0 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { removeCSS, updateCSS } from '@rc-component/util/lib/Dom/dynamicCSS';
|
||||
|
||||
import theme from '../../components/theme';
|
||||
|
||||
const duration = 0.5;
|
||||
const viewTransitionStyle = `
|
||||
@keyframes keepAlive {100% { z-index: -1 }}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: keepAlive ${duration}s linear;
|
||||
animation-fill-mode: forwards;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
.dark::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dark::view-transition-new(root) {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const useThemeAnimation = () => {
|
||||
const {
|
||||
token: { colorBgElevated },
|
||||
} = theme.useToken();
|
||||
|
||||
const animateRef = useRef<{ colorBgElevated: string }>({ colorBgElevated });
|
||||
|
||||
const startAnimationTheme = (clipPath: string[], isDark: boolean) => {
|
||||
updateCSS(
|
||||
`
|
||||
* {
|
||||
transition: none !important;
|
||||
}
|
||||
`,
|
||||
'disable-transition',
|
||||
);
|
||||
|
||||
document.documentElement
|
||||
.animate(
|
||||
{
|
||||
clipPath: isDark ? [...clipPath].reverse() : clipPath,
|
||||
},
|
||||
{
|
||||
duration: duration * 1000,
|
||||
easing: 'ease-in',
|
||||
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)',
|
||||
},
|
||||
)
|
||||
.addEventListener('finish', () => {
|
||||
removeCSS('disable-transition');
|
||||
});
|
||||
};
|
||||
|
||||
const toggleAnimationTheme = (
|
||||
event: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||
isDark: boolean,
|
||||
) => {
|
||||
if (!(event && typeof document.startViewTransition === 'function')) {
|
||||
return;
|
||||
}
|
||||
const time = Date.now();
|
||||
const x = event.clientX;
|
||||
const y = event.clientY;
|
||||
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
|
||||
updateCSS(
|
||||
`
|
||||
[data-prefers-color='dark'] {
|
||||
color-scheme: light !important;
|
||||
}
|
||||
|
||||
[data-prefers-color='light'] {
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
`,
|
||||
'color-scheme',
|
||||
);
|
||||
document
|
||||
.startViewTransition(async () => {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove(isDark ? 'dark' : 'light');
|
||||
root.classList.add(isDark ? 'light' : 'dark');
|
||||
})
|
||||
.ready.then(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Theme transition finished in ${Date.now() - time}ms`);
|
||||
const clipPath = [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${endRadius}px at ${x}px ${y}px)`,
|
||||
];
|
||||
removeCSS('color-scheme');
|
||||
startAnimationTheme(clipPath, isDark);
|
||||
});
|
||||
};
|
||||
|
||||
// inject transition style
|
||||
useEffect(() => {
|
||||
if (typeof document.startViewTransition === 'function') {
|
||||
updateCSS(viewTransitionStyle, 'view-transition-style');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (colorBgElevated !== animateRef.current.colorBgElevated) {
|
||||
animateRef.current.colorBgElevated = colorBgElevated;
|
||||
}
|
||||
}, [colorBgElevated]);
|
||||
|
||||
return toggleAnimationTheme;
|
||||
};
|
||||
|
||||
export default useThemeAnimation;
|
||||
@@ -1,3 +0,0 @@
|
||||
// must be .js file, can't modify to be .ts file!
|
||||
|
||||
export { default } from './theme/common/Loading';
|
||||
@@ -1,59 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { HomeOutlined } from '@ant-design/icons';
|
||||
import { Button, Result } from 'antd';
|
||||
import { useLocation } from 'dumi';
|
||||
|
||||
import Link from '../../theme/common/Link';
|
||||
import * as utils from '../../theme/utils';
|
||||
|
||||
export interface NotFoundProps {
|
||||
router: {
|
||||
push: (pathname: string) => void;
|
||||
replace: (pathname: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const DIRECT_MAP: Record<string, string> = {
|
||||
'docs/spec/download': 'docs/resources',
|
||||
'docs/spec/work-with-us': 'docs/resources',
|
||||
};
|
||||
|
||||
const NotFoundPage: React.FC<NotFoundProps> = ({ 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Report if necessary
|
||||
const { yuyanMonitor } = window as any;
|
||||
yuyanMonitor?.log({
|
||||
code: 11,
|
||||
msg: `Page not found: ${location.href}; Source: ${document.referrer}`,
|
||||
});
|
||||
}, [isZhCN, pathname, router]);
|
||||
|
||||
return (
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle={isZhCN ? '你访问的页面貌似不存在?' : 'Sorry, the page you visited does not exist.'}
|
||||
extra={
|
||||
<Link to={utils.getLocalizedPathname('/', isZhCN)}>
|
||||
<Button type="primary" icon={<HomeOutlined />}>
|
||||
{isZhCN ? '返回 Ant Design 首页' : 'Back to home page'}
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
||||
@@ -1,3 +0,0 @@
|
||||
import Homepage from '../index/index';
|
||||
|
||||
export default Homepage;
|
||||
@@ -1,201 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Alert, Badge, Carousel, Flex, Skeleton, Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import SiteContext from '../../../theme/slots/SiteContext';
|
||||
import type { Extra, Icon } from './util';
|
||||
import { getCarouselStyle, useAntdSiteConfig } from './util';
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css, cx }) => {
|
||||
const { carousel } = getCarouselStyle();
|
||||
|
||||
const itemBase = css`
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
text-decoration: none;
|
||||
background: ${cssVar.colorBgContainer};
|
||||
border: ${cssVar.lineWidth} solid ${cssVar.colorBorderSecondary};
|
||||
border-radius: ${cssVar.borderRadiusLG};
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
padding-block: ${cssVar.paddingMD};
|
||||
padding-inline: ${cssVar.paddingLG};
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
return {
|
||||
itemBase,
|
||||
ribbon: css`
|
||||
& > .${cx(itemBase)} {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
cardItem: css`
|
||||
&:hover {
|
||||
box-shadow: ${cssVar.boxShadowCard};
|
||||
border-color: transparent;
|
||||
}
|
||||
`,
|
||||
sliderItem: css`
|
||||
margin: 0 ${cssVar.margin};
|
||||
text-align: start;
|
||||
`,
|
||||
container: css`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-inline: auto;
|
||||
box-sizing: border-box;
|
||||
column-gap: calc(${cssVar.paddingMD} * 2);
|
||||
align-items: stretch;
|
||||
text-align: start;
|
||||
min-height: 178px;
|
||||
> * {
|
||||
width: calc((100% - calc(${cssVar.marginXXL} * 2)) / 3);
|
||||
}
|
||||
`,
|
||||
carousel,
|
||||
bannerBg: css`
|
||||
height: ${cssVar.fontSize};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface RecommendItemProps {
|
||||
extra: Extra;
|
||||
index: number;
|
||||
icons?: Icon[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const RecommendItem: React.FC<RecommendItemProps> = (props) => {
|
||||
const { extra, index, icons, className } = props;
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
if (!extra) {
|
||||
return <Skeleton key={index} />;
|
||||
}
|
||||
|
||||
const icon = icons?.find((i) => i.name === extra.source);
|
||||
|
||||
const card = (
|
||||
<a
|
||||
key={extra?.title}
|
||||
href={extra.href}
|
||||
target="_blank"
|
||||
className={clsx(styles.itemBase, className)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Typography.Title level={5}>{extra?.title}</Typography.Title>
|
||||
<Typography.Paragraph type="secondary" style={{ flex: 'auto' }}>
|
||||
{extra.description}
|
||||
</Typography.Paragraph>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Typography.Text>{extra.date}</Typography.Text>
|
||||
{icon?.href && (
|
||||
<img src={icon.href} draggable={false} className={styles.bannerBg} alt="banner" />
|
||||
)}
|
||||
</Flex>
|
||||
</a>
|
||||
);
|
||||
|
||||
if (index === 0) {
|
||||
return (
|
||||
<Badge.Ribbon text="HOT" color="red" rootClassName={styles.ribbon}>
|
||||
{card}
|
||||
</Badge.Ribbon>
|
||||
);
|
||||
}
|
||||
|
||||
return card;
|
||||
};
|
||||
|
||||
export const BannerRecommendsFallback: React.FC = () => {
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
const list = Array.from({ length: 3 });
|
||||
|
||||
return isMobile ? (
|
||||
<Carousel className={styles.carousel}>
|
||||
{list.map((_, index) => (
|
||||
<div key={`mobile-${index}`} className={styles.itemBase}>
|
||||
<Skeleton active style={{ padding: '0 24px' }} />
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
) : (
|
||||
<div className={styles.container}>
|
||||
{list.map((_, index) => (
|
||||
<div key={`desktop-${index}`} className={styles.itemBase}>
|
||||
<Skeleton active />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BannerRecommends: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
const [, lang] = useLocale();
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
const { data, error, isLoading } = useAntdSiteConfig();
|
||||
|
||||
if (isLoading) {
|
||||
return <BannerRecommendsFallback />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert
|
||||
showIcon
|
||||
type="error"
|
||||
title={error.message}
|
||||
description={process.env.NODE_ENV !== 'production' ? error.stack : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const extras = data?.extras?.[lang];
|
||||
|
||||
const mergedExtras =
|
||||
!extras || !extras.length ? Array.from<Extra>({ length: 3 }) : extras.slice(0, 3);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Carousel className={styles.carousel}>
|
||||
{mergedExtras.map((extra, index) => (
|
||||
<div key={`mobile-${index}`}>
|
||||
<RecommendItem
|
||||
extra={extra}
|
||||
index={index}
|
||||
icons={data?.icons}
|
||||
className={styles.sliderItem}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{mergedExtras.map((extra, index) => (
|
||||
<RecommendItem
|
||||
key={`desktop-${index}`}
|
||||
extra={extra}
|
||||
index={index}
|
||||
icons={data?.icons}
|
||||
className={styles.cardItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BannerRecommends;
|
||||
@@ -1,372 +0,0 @@
|
||||
import React from 'react';
|
||||
import { CustomerServiceOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Card,
|
||||
Carousel,
|
||||
DatePicker,
|
||||
Flex,
|
||||
FloatButton,
|
||||
Masonry,
|
||||
Splitter,
|
||||
Tag,
|
||||
Tour,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import SiteContext from '../../../theme/slots/SiteContext';
|
||||
import { DarkContext } from './../../../hooks/useDark';
|
||||
import { getCarouselStyle } from './util';
|
||||
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: DatePickerDoNotUseOrYouWillBeFired } = DatePicker;
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: TourDoNotUseOrYouWillBeFired } = Tour;
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: FloatButtonDoNotUseOrYouWillBeFired } = FloatButton;
|
||||
|
||||
const SAMPLE_CONTENT_EN =
|
||||
'Ant Design use CSS-in-JS technology to provide dynamic & mix theme ability. And which use component level CSS-in-JS solution get your application a better performance.';
|
||||
|
||||
const SAMPLE_CONTENT_CN =
|
||||
'Ant Design 使用 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 = createStyles(({ cssVar }, isDark: boolean) => {
|
||||
const { carousel } = getCarouselStyle();
|
||||
return {
|
||||
card: css`
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
border: 1px solid ${isDark ? cssVar.colorBorder : 'transparent'};
|
||||
background-color: ${isDark ? cssVar.colorBgContainer : '#f5f8ff'};
|
||||
padding: ${cssVar.paddingXL};
|
||||
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;
|
||||
`,
|
||||
mobileCard: css`
|
||||
height: 395px;
|
||||
`,
|
||||
nodeWrap: css`
|
||||
margin-top: ${cssVar.paddingLG};
|
||||
flex: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
carousel,
|
||||
componentsList: css`
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`,
|
||||
mobileComponentsList: css`
|
||||
margin: 0 ${cssVar.margin};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const ComponentItem: React.FC<ComponentItemProps> = ({ title, node, type, index }) => {
|
||||
const tagColor = type === 'new' ? 'processing' : 'warning';
|
||||
const [locale] = useLocale(locales);
|
||||
const tagText = type === 'new' ? locale.new : locale.update;
|
||||
const isDark = React.use(DarkContext);
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
const { styles } = useStyle(isDark);
|
||||
return (
|
||||
<div className={clsx(styles.card, isMobile && styles.mobileCard)}>
|
||||
{/* Decorator */}
|
||||
<div
|
||||
className={styles.cardCircle}
|
||||
style={{ insetInlineEnd: (index % 2) * -20 - 20, bottom: (index % 3) * -40 - 20 }}
|
||||
/>
|
||||
|
||||
{/* Title */}
|
||||
<Flex align="center" gap="small">
|
||||
<Typography.Title level={4} style={{ fontWeight: 'normal', margin: 0 }}>
|
||||
{title}
|
||||
</Typography.Title>
|
||||
<Tag color={tagColor}>{tagText}</Tag>
|
||||
</Flex>
|
||||
<div className={styles.nodeWrap}>{node}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ComponentItemProps {
|
||||
title: React.ReactNode;
|
||||
node: React.ReactNode;
|
||||
type: 'new' | 'update';
|
||||
index: number;
|
||||
}
|
||||
|
||||
const ComponentsList: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
const [locale] = useLocale(locales);
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
const isDark = React.use(DarkContext);
|
||||
const COMPONENTS = React.useMemo<Omit<ComponentItemProps, 'index'>[]>(
|
||||
() => [
|
||||
// {
|
||||
// title: 'Modal',
|
||||
// type: 'update',
|
||||
// node: (
|
||||
// <ModalDoNotUseOrYouWillBeFired title="Ant Design" width={300}>
|
||||
// {locale.sampleContent}
|
||||
// </ModalDoNotUseOrYouWillBeFired>
|
||||
// ),
|
||||
// },
|
||||
|
||||
{
|
||||
title: 'DatePicker',
|
||||
type: 'update',
|
||||
node: (
|
||||
<DatePickerDoNotUseOrYouWillBeFired
|
||||
value={dayjs('2025-11-22 00:00:00')}
|
||||
// defaultValue={dayjs('2025-11-22 00:00:00')}
|
||||
showToday={false}
|
||||
presets={
|
||||
isMobile
|
||||
? []
|
||||
: [
|
||||
{ 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') },
|
||||
]
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// title: 'Progress',
|
||||
// type: 'update',
|
||||
// node: (
|
||||
// <Flex gap="small" vertical>
|
||||
// <Flex gap="small" align="center">
|
||||
// <Progress type="circle" railColor="#e6f4ff" percent={60} size={14} />
|
||||
// {locale.inProgress}
|
||||
// </Flex>
|
||||
// <Flex gap="small" align="center">
|
||||
// <Progress type="circle" percent={100} size={14} />
|
||||
// {locale.success}
|
||||
// </Flex>
|
||||
// <Flex gap="small" align="center">
|
||||
// <Progress type="circle" status="exception" percent={88} size={14} />
|
||||
// {locale.taskFailed}
|
||||
// </Flex>
|
||||
// </Flex>
|
||||
// ),
|
||||
// },
|
||||
|
||||
{
|
||||
title: 'Tour',
|
||||
type: 'update',
|
||||
node: (
|
||||
<TourDoNotUseOrYouWillBeFired
|
||||
title="Ant Design"
|
||||
description={locale.tour}
|
||||
style={{ width: isMobile ? 'auto' : 350 }}
|
||||
current={3}
|
||||
total={9}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: 'FloatButton',
|
||||
type: 'update',
|
||||
node: (
|
||||
<Flex align="center" gap="large">
|
||||
<FloatButtonDoNotUseOrYouWillBeFired
|
||||
shape="square"
|
||||
items={[
|
||||
{ icon: <QuestionCircleOutlined /> },
|
||||
{ icon: <CustomerServiceOutlined /> },
|
||||
{ icon: <SyncOutlined /> },
|
||||
]}
|
||||
/>
|
||||
<FloatButtonDoNotUseOrYouWillBeFired backTop />
|
||||
<FloatButtonDoNotUseOrYouWillBeFired
|
||||
items={[
|
||||
{ icon: <QuestionCircleOutlined /> },
|
||||
{ icon: <CustomerServiceOutlined /> },
|
||||
{ icon: <SyncOutlined /> },
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
|
||||
// {
|
||||
// title: 'Steps',
|
||||
// type: 'update',
|
||||
// node: <Button style={{ width: PLACEHOLDER_WIDTH }}>Placeholder</Button>,
|
||||
// },
|
||||
|
||||
{
|
||||
title: 'Splitter',
|
||||
type: 'new',
|
||||
node: (
|
||||
<Splitter
|
||||
orientation="vertical"
|
||||
style={{
|
||||
height: 320,
|
||||
width: 200,
|
||||
background: isDark ? '#1f1f1f' : '#ffffff',
|
||||
boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
>
|
||||
<Splitter.Panel defaultSize="40%" min="20%" max="70%">
|
||||
<Flex justify="center" align="center" style={{ height: '100%' }}>
|
||||
<Typography.Title type="secondary" level={5} style={{ whiteSpace: 'nowrap' }}>
|
||||
First
|
||||
</Typography.Title>
|
||||
</Flex>
|
||||
</Splitter.Panel>
|
||||
|
||||
<Splitter.Panel>
|
||||
<Flex justify="center" align="center" style={{ height: '100%' }}>
|
||||
<Typography.Title type="secondary" level={5} style={{ whiteSpace: 'nowrap' }}>
|
||||
Second
|
||||
</Typography.Title>
|
||||
</Flex>
|
||||
</Splitter.Panel>
|
||||
</Splitter>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Masonry',
|
||||
type: 'new',
|
||||
node: (
|
||||
<Masonry
|
||||
columns={2}
|
||||
gutter={8}
|
||||
style={{
|
||||
width: 300,
|
||||
height: 320,
|
||||
}}
|
||||
items={[
|
||||
{ key: '1', data: 80 },
|
||||
{ key: '2', data: 60 },
|
||||
{ key: '3', data: 40 },
|
||||
{ key: '4', data: 120 },
|
||||
{ key: '5', data: 90 },
|
||||
{ key: '6', data: 40 },
|
||||
{ key: '7', data: 60 },
|
||||
{ key: '8', data: 70 },
|
||||
{ key: '9', data: 120 },
|
||||
]}
|
||||
itemRender={({ data, index }) => (
|
||||
<Card size="small" style={{ height: data }}>
|
||||
{index + 1}
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
// {
|
||||
// title: 'Alert',
|
||||
// type: 'update',
|
||||
// node: (
|
||||
// <Alert
|
||||
// style={{ width: 400 }}
|
||||
// title="Ant Design"
|
||||
// description={locale.sampleContent}
|
||||
// closable={{ closeIcon: true, disabled: true }}
|
||||
// />
|
||||
// ),
|
||||
// },
|
||||
],
|
||||
[
|
||||
isDark,
|
||||
isMobile,
|
||||
locale.inProgress,
|
||||
locale.lastMonth,
|
||||
locale.lastWeek,
|
||||
locale.lastYear,
|
||||
locale.sampleContent,
|
||||
locale.success,
|
||||
locale.taskFailed,
|
||||
locale.tour,
|
||||
locale.yesterday,
|
||||
],
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
<div className={styles.mobileComponentsList}>
|
||||
<Carousel className={styles.carousel}>
|
||||
{COMPONENTS.map<React.ReactNode>(({ title, node, type }, index) => (
|
||||
<ComponentItem
|
||||
title={title}
|
||||
node={node}
|
||||
type={type}
|
||||
index={index}
|
||||
key={`mobile-item-${index}`}
|
||||
/>
|
||||
))}
|
||||
</Carousel>
|
||||
</div>
|
||||
) : (
|
||||
<Flex justify="center" className={styles.componentsList}>
|
||||
<Flex align="stretch" gap="large">
|
||||
{COMPONENTS.map<React.ReactNode>(({ title, node, type }, index) => (
|
||||
<ComponentItem
|
||||
title={title}
|
||||
node={node}
|
||||
type={type}
|
||||
index={index}
|
||||
key={`desktop-item-${index}`}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentsList;
|
||||
@@ -1,186 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Col, Row, Typography } from 'antd';
|
||||
import { createStyles, useTheme } from 'antd-style';
|
||||
import { useLocation } from 'dumi';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import Link from '../../../theme/common/Link';
|
||||
import SiteContext from '../../../theme/slots/SiteContext';
|
||||
import * as utils from '../../../theme/utils';
|
||||
import { DarkContext } from './../../../hooks/useDark';
|
||||
|
||||
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,
|
||||
scaleOrigin: '15px',
|
||||
},
|
||||
{
|
||||
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 = createStyles(({ cssVar, css }, isDark: boolean) => {
|
||||
return {
|
||||
card: css`
|
||||
padding: ${cssVar.paddingSM};
|
||||
border-radius: calc(${cssVar.borderRadius} * 2);
|
||||
background: ${isDark ? 'rgba(0, 0, 0, 0.45)' : cssVar.colorBgElevated};
|
||||
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: ${cssVar.borderRadius};
|
||||
}
|
||||
`,
|
||||
|
||||
cardMini: css`
|
||||
display: block;
|
||||
border-radius: calc(${cssVar.borderRadius} * 2);
|
||||
padding: ${cssVar.paddingMD} ${cssVar.paddingLG};
|
||||
background: ${isDark ? 'rgba(0, 0, 0, 0.25)' : 'rgba(0, 0, 0, 0.02)'};
|
||||
border: 1px solid ${isDark ? 'rgba(255, 255, 255, 0.45)' : 'rgba(0, 0, 0, 0.06)'};
|
||||
|
||||
img {
|
||||
height: 48px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const DesignFramework: React.FC = () => {
|
||||
const [locale] = useLocale(locales);
|
||||
const token = useTheme();
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
const isDark = React.use(DarkContext);
|
||||
const { styles } = useStyle(isDark);
|
||||
const { pathname, search } = useLocation();
|
||||
const isZhCN = utils.isZhCN(pathname);
|
||||
|
||||
const colSpan = isMobile ? 24 : 8;
|
||||
|
||||
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={colSpan}>
|
||||
<Link to={path}>
|
||||
<div className={styles.card}>
|
||||
<img draggable={false} 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, scaleOrigin }, index) => {
|
||||
const title = locale[key as keyof typeof locale];
|
||||
const desc = locale[`${key}Desc` as keyof typeof locale];
|
||||
|
||||
return (
|
||||
<Col key={index} span={colSpan}>
|
||||
<a className={styles.cardMini} target="_blank" href={url} rel="noreferrer">
|
||||
<img
|
||||
draggable={false}
|
||||
alt={title}
|
||||
src={img}
|
||||
style={{ transform: `scale(${imgScale})`, transformOrigin: scaleOrigin }}
|
||||
/>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesignFramework;
|
||||
@@ -1,84 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { createStaticStyles, useTheme } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import SiteContext from '../../../theme/slots/SiteContext';
|
||||
import GroupMaskLayer from './GroupMaskLayer';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
box: css`
|
||||
position: relative;
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
`,
|
||||
container: css`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
`,
|
||||
typographyWrapper: css`
|
||||
text-align: center;
|
||||
`,
|
||||
marginStyle: css`
|
||||
max-width: 1208px;
|
||||
margin-inline: auto;
|
||||
box-sizing: border-box;
|
||||
padding-inline: ${cssVar.marginXXL};
|
||||
`,
|
||||
withoutChildren: css`
|
||||
min-height: 300px;
|
||||
border-radius: ${cssVar.borderRadiusLG};
|
||||
background-color: '#e9e9e9';
|
||||
`,
|
||||
}));
|
||||
|
||||
export interface GroupProps {
|
||||
id?: string;
|
||||
title?: React.ReactNode;
|
||||
titleColor?: string;
|
||||
description?: React.ReactNode;
|
||||
background?: string;
|
||||
/** 是否不使用两侧 margin */
|
||||
collapse?: boolean;
|
||||
decoration?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
|
||||
const { id, title, titleColor, description, children, decoration, background, collapse } = props;
|
||||
const token = useTheme();
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
return (
|
||||
<div style={{ backgroundColor: background }} className={styles.box}>
|
||||
<div className={styles.container}>{decoration}</div>
|
||||
<GroupMaskLayer style={{ paddingBlock: token.marginFarSM }}>
|
||||
<div className={styles.typographyWrapper}>
|
||||
<Typography.Title
|
||||
id={id}
|
||||
level={1}
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
color: titleColor,
|
||||
// Special for the title
|
||||
fontSize: isMobile ? token.fontSizeHeading2 : token.fontSizeHeading1,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
color: titleColor,
|
||||
marginBottom: isMobile ? token.marginXXL : token.marginFarXS,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<div className={clsx({ [styles.marginStyle]: !collapse })}>
|
||||
{children ? <div>{children}</div> : <div className={styles.withoutChildren} />}
|
||||
</div>
|
||||
</GroupMaskLayer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Group;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
siteMask: css`
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
`,
|
||||
}));
|
||||
|
||||
export interface GroupMaskLayerProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onMouseMove?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const GroupMaskLayer: React.FC<React.PropsWithChildren<GroupMaskLayerProps>> = (props) => {
|
||||
const { children, className, style, onMouseMove, onMouseEnter, onMouseLeave } = props;
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={clsx(className, classNames.siteMask)}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupMaskLayer;
|
||||
@@ -1,220 +0,0 @@
|
||||
import React from 'react';
|
||||
import { AntDesignOutlined, CheckOutlined, CloseOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
ColorPicker,
|
||||
Dropdown,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Progress,
|
||||
Select,
|
||||
Slider,
|
||||
Space,
|
||||
Steps,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
import useLocale from '../../../../hooks/useLocale';
|
||||
import Tilt from './Tilt';
|
||||
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: ModalPanel } = Modal;
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: InternalTooltip } = Tooltip;
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: InternalMessage } = message;
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
range: '设置范围',
|
||||
text: 'Ant Design 使用 CSS-in-JS 技术以提供动态与混合主题的能力。与此同时,我们使用组件级别的 CSS-in-JS 解决方案,让你的应用获得更好的性能。',
|
||||
infoText: '信息内容展示',
|
||||
dropdown: '下拉菜单',
|
||||
finished: '已完成',
|
||||
inProgress: '进行中',
|
||||
waiting: '等待中',
|
||||
option: '选项',
|
||||
apple: '苹果',
|
||||
banana: '香蕉',
|
||||
orange: '橘子',
|
||||
watermelon: '西瓜',
|
||||
primary: '主要按钮',
|
||||
danger: '危险按钮',
|
||||
default: '默认按钮',
|
||||
dashed: '虚线按钮',
|
||||
icon: '图标按钮',
|
||||
hello: '你好,Ant Design!',
|
||||
release: 'Ant Design 6.0 正式发布!',
|
||||
},
|
||||
en: {
|
||||
range: 'Set Range',
|
||||
text: 'Ant Design use CSS-in-JS technology to provide dynamic & mix theme ability. And which use component level CSS-in-JS solution get your application a better performance.',
|
||||
infoText: 'Info Text',
|
||||
dropdown: 'Dropdown',
|
||||
finished: 'Finished',
|
||||
inProgress: 'In Progress',
|
||||
waiting: 'Waiting',
|
||||
option: 'Option',
|
||||
apple: 'Apple',
|
||||
banana: 'Banana',
|
||||
orange: 'Orange',
|
||||
watermelon: 'Watermelon',
|
||||
primary: 'Primary',
|
||||
danger: 'Danger',
|
||||
default: 'Default',
|
||||
dashed: 'Dashed',
|
||||
icon: 'Icon',
|
||||
hello: 'Hello, Ant Design!',
|
||||
release: 'Ant Design 6.0 is released!',
|
||||
},
|
||||
};
|
||||
|
||||
const styles = createStaticStyles(({ cssVar, css }) => {
|
||||
const gap = cssVar.padding;
|
||||
return {
|
||||
holder: css`
|
||||
width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: ${gap};
|
||||
opacity: 0.8;
|
||||
`,
|
||||
flex: css`
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
column-gap: ${gap};
|
||||
`,
|
||||
ptg_20: css`
|
||||
flex: 0 1 20%;
|
||||
`,
|
||||
ptg_none: css`
|
||||
flex: none;
|
||||
`,
|
||||
block: css`
|
||||
background-color: ${cssVar.colorBgContainer};
|
||||
padding: ${cssVar.paddingXS} ${cssVar.paddingSM};
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
border: 1px solid ${cssVar.colorBorder};
|
||||
`,
|
||||
noMargin: css`
|
||||
margin: 0;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const ComponentsBlock: React.FC = () => {
|
||||
const [locale] = useLocale(locales);
|
||||
|
||||
return (
|
||||
<Tilt options={{ max: 4, glare: false, scale: 0.98 }} className={styles.holder}>
|
||||
<ModalPanel title="Ant Design" width="100%">
|
||||
{locale.text}
|
||||
</ModalPanel>
|
||||
<Alert title={locale.infoText} type="info" />
|
||||
{/* Line */}
|
||||
<div className={styles.flex}>
|
||||
<ColorPicker style={{ flex: 'none' }} />
|
||||
<div style={{ flex: 'none' }}>
|
||||
<Space.Compact>
|
||||
<Button>{locale.dropdown}</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: Array.from({ length: 5 }).map((_, index) => ({
|
||||
key: `opt${index}`,
|
||||
label: `${locale.option} ${index}`,
|
||||
})),
|
||||
}}
|
||||
>
|
||||
<Button icon={<DownOutlined />} />
|
||||
</Dropdown>
|
||||
</Space.Compact>
|
||||
</div>
|
||||
<Select
|
||||
style={{ flex: 'auto' }}
|
||||
mode="multiple"
|
||||
maxTagCount="responsive"
|
||||
defaultValue={[{ value: 'apple' }, { value: 'banana' }]}
|
||||
options={[
|
||||
{ value: 'apple', label: locale.apple },
|
||||
{ value: 'banana', label: locale.banana },
|
||||
{ value: 'orange', label: locale.orange },
|
||||
{ value: 'watermelon', label: locale.watermelon },
|
||||
]}
|
||||
/>
|
||||
<Input style={{ flex: 'none', width: 120 }} />
|
||||
</div>
|
||||
<Progress
|
||||
style={{ margin: 0 }}
|
||||
percent={100}
|
||||
strokeColor={{ '0%': '#108ee9', '100%': '#87d068' }}
|
||||
/>
|
||||
<Progress style={{ margin: 0 }} percent={33} status="exception" />
|
||||
<Steps
|
||||
current={1}
|
||||
items={[
|
||||
{ title: locale.finished },
|
||||
{ title: locale.inProgress },
|
||||
{ title: locale.waiting },
|
||||
]}
|
||||
/>
|
||||
{/* Line */}
|
||||
<div className={styles.block}>
|
||||
<Slider
|
||||
style={{ marginInline: 20 }}
|
||||
range
|
||||
marks={{
|
||||
0: '0°C',
|
||||
26: '26°C',
|
||||
37: '37°C',
|
||||
100: {
|
||||
style: { color: '#f50' },
|
||||
label: <strong>100°C</strong>,
|
||||
},
|
||||
}}
|
||||
defaultValue={[26, 37]}
|
||||
/>
|
||||
</div>
|
||||
{/* Line */}
|
||||
<div className={styles.flex}>
|
||||
<Button className={styles.ptg_20} type="primary">
|
||||
{locale.primary}
|
||||
</Button>
|
||||
<Button className={styles.ptg_20} type="primary" danger>
|
||||
{locale.danger}
|
||||
</Button>
|
||||
<Button className={styles.ptg_20}>{locale.default}</Button>
|
||||
<Button className={styles.ptg_20} type="dashed">
|
||||
{locale.dashed}
|
||||
</Button>
|
||||
<Button className={styles.ptg_20} icon={<AntDesignOutlined />}>
|
||||
{locale.icon}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Line */}
|
||||
<div className={styles.block}>
|
||||
<div className={styles.flex}>
|
||||
<Switch
|
||||
className={styles.ptg_none}
|
||||
defaultChecked
|
||||
checkedChildren={<CheckOutlined />}
|
||||
unCheckedChildren={<CloseOutlined />}
|
||||
/>
|
||||
<Checkbox.Group
|
||||
className={styles.ptg_none}
|
||||
options={[locale.apple, locale.banana, locale.orange]}
|
||||
defaultValue={[locale.apple]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<InternalMessage content={locale.release} type="success" />
|
||||
</div>
|
||||
<InternalTooltip title={locale.hello} placement="topLeft" className={styles.noMargin} />
|
||||
<Alert title="Ant Design love you!" type="success" />
|
||||
</Tilt>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentsBlock;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import VanillaTilt from 'vanilla-tilt';
|
||||
import type { TiltOptions } from 'vanilla-tilt';
|
||||
|
||||
interface TiltProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
options?: TiltOptions;
|
||||
}
|
||||
|
||||
// https://micku7zu.github.io/vanilla-tilt.js/index.html
|
||||
const defaultTiltOptions: TiltOptions = {
|
||||
scale: 1.02,
|
||||
max: 8,
|
||||
speed: 1500,
|
||||
glare: true,
|
||||
'max-glare': 0.8,
|
||||
};
|
||||
|
||||
const Tilt: React.FC<TiltProps> = ({ options, ...props }) => {
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (node.current) {
|
||||
VanillaTilt.init(node.current, {
|
||||
...defaultTiltOptions,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
(node.current as any)?.vanillaTilt.destroy();
|
||||
};
|
||||
}, []);
|
||||
return <div ref={node} {...props} />;
|
||||
};
|
||||
|
||||
export default Tilt;
|
||||
@@ -1,185 +0,0 @@
|
||||
import React, { Suspense, use } from 'react';
|
||||
import { Flex, Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
import { useLocation } from 'dumi';
|
||||
|
||||
import useLocale from '../../../../hooks/useLocale';
|
||||
import LinkButton from '../../../../theme/common/LinkButton';
|
||||
import SiteContext from '../../../../theme/slots/SiteContext';
|
||||
import type { SiteContextProps } from '../../../../theme/slots/SiteContext';
|
||||
import * as utils from '../../../../theme/utils';
|
||||
import GroupMaskLayer from '../GroupMaskLayer';
|
||||
|
||||
import '../SiteContext';
|
||||
|
||||
const ComponentsBlock = React.lazy(() => import('./ComponentsBlock'));
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css, cx }, siteConfig: SiteContextProps) => {
|
||||
const textShadow = `0 0 4px ${cssVar.colorBgContainer}`;
|
||||
const mask = cx(css`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
backdrop-filter: blur(2px);
|
||||
opacity: 1;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transition: all 1s ease;
|
||||
pointer-events: none;
|
||||
[data-prefers-color='dark'] & {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
`);
|
||||
|
||||
const block = cx(css`
|
||||
position: absolute;
|
||||
inset-inline-end: -60px;
|
||||
top: -24px;
|
||||
transition: all 1s cubic-bezier(0.03, 0.98, 0.52, 0.99);
|
||||
`);
|
||||
|
||||
return {
|
||||
holder: css`
|
||||
height: 640px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
perspective: 800px;
|
||||
/* fix safari bug by removing blur style */
|
||||
transform: translateZ(1000px);
|
||||
row-gap: ${cssVar.marginXL};
|
||||
|
||||
&:hover {
|
||||
.${mask} {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.${block} {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
mask,
|
||||
|
||||
typography: css`
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-inline: ${cssVar.paddingXL};
|
||||
text-shadow: ${Array.from({ length: 5 }, () => textShadow).join(', ')};
|
||||
h1 {
|
||||
font-weight: 900 !important;
|
||||
font-size: calc(${cssVar.fontSizeHeading2} * 2) !important;
|
||||
line-height: ${cssVar.lineHeightHeading2} !important;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: ${cssVar.fontSizeLG} !important;
|
||||
font-weight: normal !important;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
block,
|
||||
child: css`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
z-index: 1;
|
||||
`,
|
||||
btnWrap: css`
|
||||
margin-bottom: ${cssVar.marginXL};
|
||||
`,
|
||||
bgImg: css`
|
||||
position: absolute;
|
||||
width: 240px;
|
||||
`,
|
||||
bgImgTop: css`
|
||||
top: 0;
|
||||
inset-inline-start: ${siteConfig.isMobile ? '-120px' : 0};
|
||||
`,
|
||||
bgImgBottom: css`
|
||||
bottom: 120px;
|
||||
inset-inline-end: ${siteConfig.isMobile ? 0 : '40%'};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const PreviewBanner: React.FC<Readonly<React.PropsWithChildren>> = (props) => {
|
||||
const { children } = props;
|
||||
const [locale] = useLocale(locales);
|
||||
const siteConfig = use(SiteContext);
|
||||
const { styles } = useStyle(siteConfig);
|
||||
const { pathname, search } = useLocation();
|
||||
const isZhCN = utils.isZhCN(pathname);
|
||||
|
||||
return (
|
||||
<GroupMaskLayer>
|
||||
{/* Image Left Top */}
|
||||
<img
|
||||
alt="bg"
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/49f963db-b2a8-4f15-857a-270d771a1204.svg"
|
||||
draggable={false}
|
||||
className={clsx(styles.bgImg, styles.bgImgTop)}
|
||||
/>
|
||||
{/* Image Right Top */}
|
||||
<img
|
||||
alt="bg"
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/e152223c-bcae-4913-8938-54fda9efe330.svg"
|
||||
draggable={false}
|
||||
className={clsx(styles.bgImg, styles.bgImgBottom)}
|
||||
/>
|
||||
|
||||
<div className={styles.holder}>
|
||||
{/* Mobile not show the component preview */}
|
||||
<Suspense fallback={null}>
|
||||
{siteConfig.isMobile ? null : (
|
||||
<div className={styles.block}>
|
||||
<ComponentsBlock />
|
||||
</div>
|
||||
)}
|
||||
</Suspense>
|
||||
<div className={styles.mask} />
|
||||
<Typography className={styles.typography}>
|
||||
<h1>Ant Design</h1>
|
||||
<p>{locale.slogan}</p>
|
||||
</Typography>
|
||||
<Flex gap="middle" className={styles.btnWrap}>
|
||||
<LinkButton
|
||||
size="large"
|
||||
type="primary"
|
||||
to={utils.getLocalizedPathname('/components/overview/', isZhCN, search)}
|
||||
>
|
||||
{locale.start}
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
size="large"
|
||||
to={utils.getLocalizedPathname('/docs/spec/introduce/', isZhCN, search)}
|
||||
>
|
||||
{locale.designLanguage}
|
||||
</LinkButton>
|
||||
</Flex>
|
||||
<div className={styles.child}>{children}</div>
|
||||
</div>
|
||||
</GroupMaskLayer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewBanner;
|
||||
@@ -1,9 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface SiteContextProps {
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
const SiteContext = React.createContext<SiteContextProps>({ isMobile: false });
|
||||
|
||||
export default SiteContext;
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { CSSMotionList } from '@rc-component/motion';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import { COLOR_IMAGES, getClosetColor } from './colorUtil';
|
||||
|
||||
export interface BackgroundImageProps {
|
||||
colorPrimary?: string;
|
||||
isLight?: boolean;
|
||||
}
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
image: css`
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: right top;
|
||||
`,
|
||||
}));
|
||||
|
||||
const onShow = () => ({ opacity: 1 });
|
||||
|
||||
const onHide = () => ({ opacity: 0 });
|
||||
|
||||
const BackgroundImage: React.FC<BackgroundImageProps> = ({ colorPrimary, isLight }) => {
|
||||
const activeColor = useMemo(() => getClosetColor(colorPrimary), [colorPrimary]);
|
||||
|
||||
const [keyList, setKeyList] = useState<string[]>([]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
setKeyList([activeColor as string]);
|
||||
}, [activeColor]);
|
||||
|
||||
return (
|
||||
<CSSMotionList
|
||||
keys={keyList}
|
||||
motionName="transition"
|
||||
onEnterStart={onHide}
|
||||
onAppearStart={onHide}
|
||||
onEnterActive={onShow}
|
||||
onAppearActive={onShow}
|
||||
onLeaveStart={onShow}
|
||||
onLeaveActive={onHide}
|
||||
motionDeadline={500}
|
||||
>
|
||||
{({ key: color, className, style }) => {
|
||||
const cls = clsx(styles.image, className);
|
||||
const entity = COLOR_IMAGES.find((ent) => ent.color === color);
|
||||
|
||||
if (!entity || !entity.url) {
|
||||
return null as unknown as React.ReactElement;
|
||||
}
|
||||
|
||||
const { opacity } = style || {};
|
||||
|
||||
return (
|
||||
<picture>
|
||||
<source srcSet={entity.webp} type="image/webp" />
|
||||
<source srcSet={entity.url} type="image/jpeg" />
|
||||
<img
|
||||
draggable={false}
|
||||
className={cls}
|
||||
style={{ ...style, opacity: isLight ? opacity : 0 }}
|
||||
src={entity.url}
|
||||
alt="bg"
|
||||
/>
|
||||
</picture>
|
||||
);
|
||||
}}
|
||||
</CSSMotionList>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackgroundImage;
|
||||
@@ -1,138 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ColorPicker, Flex, Input } from 'antd';
|
||||
import type { ColorPickerProps, GetProp } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { generateColor } from 'antd/es/color-picker/util';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import { PRESET_COLORS } from './colorUtil';
|
||||
|
||||
type Color = Extract<GetProp<ColorPickerProps, 'value'>, string | { cleared: any }>;
|
||||
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
color: css`
|
||||
width: calc(${cssVar.controlHeightLG} / 2);
|
||||
height: calc(${cssVar.controlHeightLG} / 2);
|
||||
border-radius: 100%;
|
||||
cursor: pointer;
|
||||
transition: all ${cssVar.motionDurationFast};
|
||||
display: inline-block;
|
||||
|
||||
& > input[type='radio'] {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
// need ?
|
||||
}
|
||||
`,
|
||||
|
||||
colorActive: css`
|
||||
box-shadow:
|
||||
0 0 0 1px ${cssVar.colorBgContainer},
|
||||
0 0 0 calc(${cssVar.controlOutlineWidth} * 2 + 1) ${cssVar.colorPrimary};
|
||||
`,
|
||||
}));
|
||||
|
||||
export interface ThemeColorPickerProps {
|
||||
id?: string;
|
||||
value?: string | Color;
|
||||
onChange?: (value?: Color | string) => void;
|
||||
}
|
||||
|
||||
const DebouncedColorPicker: React.FC<React.PropsWithChildren<ThemeColorPickerProps>> = (props) => {
|
||||
const { value: color, children, onChange } = props;
|
||||
const [value, setValue] = useState(color);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
onChange?.(value);
|
||||
}, 200);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(color);
|
||||
}, [color]);
|
||||
|
||||
return (
|
||||
<ColorPicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
presets={[{ label: 'PresetColors', key: 'PresetColors', colors: PRESET_COLORS }]}
|
||||
>
|
||||
{children}
|
||||
</ColorPicker>
|
||||
);
|
||||
};
|
||||
|
||||
const ThemeColorPicker: React.FC<ThemeColorPickerProps> = ({ value, onChange, id }) => {
|
||||
const matchColors = React.useMemo(() => {
|
||||
const valueStr = generateColor(value || '').toRgbString();
|
||||
const colors = PRESET_COLORS.map((color) => {
|
||||
const colorStr = generateColor(color).toRgbString();
|
||||
const active = colorStr === valueStr;
|
||||
return { color, active, picker: false } as const;
|
||||
});
|
||||
const existActive = colors.some((c) => c.active);
|
||||
|
||||
return [
|
||||
...colors,
|
||||
{
|
||||
color: 'conic-gradient(red, yellow, lime, aqua, blue, magenta, red)',
|
||||
picker: true,
|
||||
active: !existActive,
|
||||
},
|
||||
];
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Flex gap="large" align="center" wrap>
|
||||
<Input
|
||||
value={typeof value === 'string' ? value : value?.toHexString()}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
style={{ width: 120 }}
|
||||
id={id}
|
||||
/>
|
||||
<Flex gap="middle">
|
||||
{matchColors.map<React.ReactNode>(({ color, active, picker }) => {
|
||||
const colorNode = (
|
||||
<label
|
||||
key={color}
|
||||
className={clsx(styles.color, { [styles.colorActive]: active })}
|
||||
style={{ background: color }}
|
||||
onClick={() => {
|
||||
if (!picker) {
|
||||
onChange?.(color);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={picker ? 'picker' : 'color'}
|
||||
aria-label={color}
|
||||
tabIndex={picker ? -1 : 0}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
return picker ? (
|
||||
<DebouncedColorPicker
|
||||
key={`colorpicker-${value}`}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
>
|
||||
{colorNode}
|
||||
</DebouncedColorPicker>
|
||||
) : (
|
||||
colorNode
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeColorPicker;
|
||||
@@ -1,122 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Carousel, Typography } from 'antd';
|
||||
import { createStyles, css, useTheme } from 'antd-style';
|
||||
|
||||
import { getCarouselStyle } from '../util';
|
||||
|
||||
const useStyle = createStyles(() => {
|
||||
const { carousel } = getCarouselStyle();
|
||||
return {
|
||||
carousel,
|
||||
container: css`
|
||||
position: relative;
|
||||
`,
|
||||
title: css`
|
||||
position: absolute;
|
||||
top: 15%;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`,
|
||||
img: css`
|
||||
width: 100%;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const mobileImageConfigList = [
|
||||
{
|
||||
imageSrc:
|
||||
'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*KsMrRZaciFcAAAAAAAAAAAAADrJ8AQ/original',
|
||||
titleColor: 'rgba(0,0,0,.88)',
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*3FkqR6XRNgoAAAAAAAAAAAAADrJ8AQ/original',
|
||||
titleColor: '#fff',
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cSX_RbD3k9wAAAAAAAAAAAAADrJ8AQ/original',
|
||||
titleColor: '#fff',
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*MldsRZeax6EAAAAAAAAAAAAADrJ8AQ/original',
|
||||
titleColor: '#fff',
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xCAmSL0xlZ8AAAAAAAAAAAAADrJ8AQ/original',
|
||||
titleColor: '#fff',
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*vCfCSbiI_VIAAAAAAAAAAAAADrJ8AQ/original',
|
||||
titleColor: '#fff',
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xCAmSL0xlZ8AAAAAAAAAAAAADrJ8AQ/original',
|
||||
titleColor: '#fff',
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*BeDBTY9UnXIAAAAAAAAAAAAADrJ8AQ/original',
|
||||
titleColor: '#fff',
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*Q63XTbk8YaMAAAAAAAAAAAAADrJ8AQ/original',
|
||||
titleColor: '#fff',
|
||||
},
|
||||
];
|
||||
|
||||
export interface MobileCarouselProps {
|
||||
id?: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
}
|
||||
|
||||
const MobileCarousel: React.FC<MobileCarouselProps> = (props) => {
|
||||
const { styles } = useStyle();
|
||||
const { id, title, description } = props;
|
||||
const token = useTheme();
|
||||
const [currentSlider, setCurrentSlider] = useState<number>(0);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.title}>
|
||||
<Typography.Title
|
||||
id={id}
|
||||
level={1}
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
color: mobileImageConfigList[currentSlider].titleColor,
|
||||
fontSize: token.fontSizeHeading2,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
marginBottom: token.marginXXL,
|
||||
color: mobileImageConfigList[currentSlider].titleColor,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<Carousel className={styles.carousel} afterChange={setCurrentSlider}>
|
||||
{mobileImageConfigList.map((item, index) => (
|
||||
<div key={index}>
|
||||
<img draggable={false} src={item.imageSrc} className={styles.img} alt="carousel" />
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileCarousel;
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Flex, InputNumber, Slider } from 'antd';
|
||||
|
||||
export interface RadiusPickerProps {
|
||||
id?: string;
|
||||
value?: number;
|
||||
onChange?: (value: number | null) => void;
|
||||
}
|
||||
|
||||
const RadiusPicker: React.FC<RadiusPickerProps> = ({ id, value, onChange }) => (
|
||||
<Flex gap="large">
|
||||
<InputNumber
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
style={{ width: 120 }}
|
||||
min={0}
|
||||
formatter={(val) => `${val}px`}
|
||||
parser={(str) => str?.replace('px', '') as unknown as number}
|
||||
id={id}
|
||||
/>
|
||||
<Slider
|
||||
tooltip={{ open: false }}
|
||||
style={{ width: 128 }}
|
||||
min={0}
|
||||
value={value}
|
||||
max={20}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
export default RadiusPicker;
|
||||
@@ -1,102 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
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',
|
||||
v4: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*bOiWT4-34jkAAAAAAAAAAAAADrJ8AQ/original',
|
||||
} as const;
|
||||
|
||||
export type THEME = keyof typeof THEMES;
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
default: '默认',
|
||||
dark: '暗黑',
|
||||
lark: '知识协作',
|
||||
comic: '桃花缘',
|
||||
v4: 'V4 主题',
|
||||
},
|
||||
en: {
|
||||
default: 'Default',
|
||||
dark: 'Dark',
|
||||
lark: 'Document',
|
||||
comic: 'Blossom',
|
||||
v4: 'V4 Theme',
|
||||
},
|
||||
};
|
||||
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
themeCard: css`
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
cursor: pointer;
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
|
||||
& > input[type='radio'] {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
&:focus-within,
|
||||
&:hover {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
`,
|
||||
|
||||
themeCardActive: css`
|
||||
box-shadow:
|
||||
0 0 0 1px ${cssVar.colorBgContainer},
|
||||
0 0 0 calc(${cssVar.controlOutlineWidth} * 2 + 1px) ${cssVar.colorPrimary};
|
||||
&,
|
||||
&:hover:not(:focus-within) {
|
||||
transform: scale(1);
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
export interface ThemePickerProps {
|
||||
id?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
const ThemePicker: React.FC<ThemePickerProps> = (props) => {
|
||||
const { value, id, onChange } = props;
|
||||
const [locale] = useLocale(locales);
|
||||
return (
|
||||
<Flex gap="large" wrap>
|
||||
{(Object.keys(THEMES) as (keyof typeof THEMES)[]).map<React.ReactNode>((theme, index) => (
|
||||
<Flex vertical gap="small" justify="center" align="center" key={theme}>
|
||||
<label
|
||||
onClick={() => onChange?.(theme)}
|
||||
className={clsx(styles.themeCard, { [styles.themeCardActive]: value === theme })}
|
||||
>
|
||||
<input type="radio" name="theme" id={index === 0 ? id : undefined} />
|
||||
<img draggable={false} src={THEMES[theme]} alt={theme} />
|
||||
</label>
|
||||
<span>{locale[theme]}</span>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemePicker;
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { ColorPickerProps, GetProp } from 'antd';
|
||||
import { generateColor } from 'antd/es/color-picker/util';
|
||||
|
||||
type Color = GetProp<ColorPickerProps, 'value'>;
|
||||
|
||||
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,
|
||||
webp: null,
|
||||
},
|
||||
{
|
||||
color: '#5A54F9',
|
||||
url: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*H8nRT7_q0EwAAAAAAAAAAAAADrJ8AQ/original',
|
||||
webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*H8nRT7_q0EwAAAAAAAAAAAAADrJ8AQ/fmt.webp',
|
||||
},
|
||||
{
|
||||
color: '#9E339F',
|
||||
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*FMluR4vJhaQAAAAAAAAAAAAAARQnAQ',
|
||||
webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*CMCMTKV51tIAAAAAAAAAAAAADrJ8AQ/fmt.webp',
|
||||
},
|
||||
{
|
||||
color: PINK_COLOR,
|
||||
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*DGZXS4YOGp0AAAAAAAAAAAAAARQnAQ',
|
||||
webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*s5OdR6wZZIkAAAAAAAAAAAAADrJ8AQ/fmt.webp',
|
||||
},
|
||||
{
|
||||
color: '#E0282E',
|
||||
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*w6xcR7MriwEAAAAAAAAAAAAAARQnAQ',
|
||||
webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HE_4Qp_XfQQAAAAAAAAAAAAADrJ8AQ/fmt.webp',
|
||||
},
|
||||
{
|
||||
color: '#F4801A',
|
||||
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*VWFOTbEyU9wAAAAAAAAAAAAAARQnAQ',
|
||||
webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xTG2QbottAQAAAAAAAAAAAAADrJ8AQ/fmt.webp',
|
||||
},
|
||||
{
|
||||
color: '#F2BD27',
|
||||
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*1yydQLzw5nYAAAAAAAAAAAAAARQnAQ',
|
||||
webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*hbPfSbF-xPIAAAAAAAAAAAAADrJ8AQ/fmt.webp',
|
||||
},
|
||||
{
|
||||
color: '#00B96B',
|
||||
url: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*XpGeRoZKGycAAAAAAAAAAAAAARQnAQ',
|
||||
webp: 'https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*iM6CQ496P3oAAAAAAAAAAAAADrJ8AQ/fmt.webp',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const PRESET_COLORS = COLOR_IMAGES.map(({ color }) => color);
|
||||
|
||||
const DISTANCE = 33;
|
||||
|
||||
export function getClosetColor(colorPrimary?: Color | string | null) {
|
||||
if (!colorPrimary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const colorPrimaryRGB = generateColor(colorPrimary).toRgb();
|
||||
|
||||
const distance = COLOR_IMAGES.map(({ color }) => {
|
||||
const colorObj = generateColor(color).toRgb();
|
||||
const dist = Math.sqrt(
|
||||
(colorObj.r - colorPrimaryRGB.r) ** 2 +
|
||||
(colorObj.g - colorPrimaryRGB.g) ** 2 +
|
||||
(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'
|
||||
);
|
||||
}
|
||||
@@ -1,619 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { defaultAlgorithm, defaultTheme } from '@ant-design/compatible';
|
||||
import { FastColor } from '@ant-design/fast-color';
|
||||
import {
|
||||
BellOutlined,
|
||||
FolderOutlined,
|
||||
HomeOutlined,
|
||||
QuestionCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColorPickerProps, GetProp, MenuProps, ThemeConfig } from 'antd';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Card,
|
||||
ConfigProvider,
|
||||
Flex,
|
||||
Form,
|
||||
Layout,
|
||||
Menu,
|
||||
Radio,
|
||||
theme,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { generateColor } from 'antd/es/color-picker/util';
|
||||
import { clsx } from 'clsx';
|
||||
import { useLocation } from 'dumi';
|
||||
|
||||
import useLocale from '../../../../hooks/useLocale';
|
||||
import LinkButton from '../../../../theme/common/LinkButton';
|
||||
import SiteContext from '../../../../theme/slots/SiteContext';
|
||||
import { getLocalizedPathname } from '../../../../theme/utils';
|
||||
import Group from '../Group';
|
||||
import { getCarouselStyle } from '../util';
|
||||
import { DarkContext } from './../../../../hooks/useDark';
|
||||
import BackgroundImage from './BackgroundImage';
|
||||
import ColorPicker from './ColorPicker';
|
||||
import { DEFAULT_COLOR, getAvatarURL, getClosetColor, PINK_COLOR } from './colorUtil';
|
||||
import MobileCarousel from './MobileCarousel';
|
||||
import RadiusPicker from './RadiusPicker';
|
||||
import type { THEME } from './ThemePicker';
|
||||
import ThemePicker from './ThemePicker';
|
||||
|
||||
type Color = Extract<GetProp<ColorPickerProps, 'value'>, string | { cleared: any }>;
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
|
||||
const TokenChecker: React.FC = () => {
|
||||
const token = theme.useToken();
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Demo Token:', token);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// ============================= Theme =============================
|
||||
const locales = {
|
||||
cn: {
|
||||
themeTitle: '定制主题,随心所欲',
|
||||
themeDesc: 'Ant Design 开放更多样式算法,让你定制主题更简单',
|
||||
|
||||
customizeTheme: '定制主题',
|
||||
myTheme: '我的主题',
|
||||
titlePrimaryColor: '主色',
|
||||
titleBorderRadius: '圆角',
|
||||
titleCompact: '宽松度',
|
||||
default: '默认',
|
||||
compact: '紧凑',
|
||||
titleTheme: '主题',
|
||||
light: '亮色',
|
||||
dark: '暗黑',
|
||||
toDef: '深度定制',
|
||||
toUse: '去使用',
|
||||
},
|
||||
en: {
|
||||
themeTitle: 'Flexible theme customization',
|
||||
themeDesc: 'Ant Design 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 styles = createStaticStyles(({ cssVar, css, cx }) => {
|
||||
const { carousel } = getCarouselStyle();
|
||||
const 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 ${cssVar.motionDurationSlow};
|
||||
`;
|
||||
|
||||
return {
|
||||
demo,
|
||||
|
||||
otherDemo: css`
|
||||
&.${cx(demo)} {
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(247, 247, 247, 0.5);
|
||||
}
|
||||
`,
|
||||
|
||||
darkDemo: css`
|
||||
&.${cx(demo)} {
|
||||
background: #000;
|
||||
}
|
||||
`,
|
||||
|
||||
larkDemo: css`
|
||||
&.${cx(demo)} {
|
||||
// background: #f7f7f7;
|
||||
background: rgba(240, 242, 245, 0.65);
|
||||
}
|
||||
`,
|
||||
comicDemo: css`
|
||||
&.${cx(demo)} {
|
||||
// background: #ffe4e6;
|
||||
background: rgba(240, 242, 245, 0.65);
|
||||
}
|
||||
`,
|
||||
|
||||
menu: css`
|
||||
margin-inline-start: auto;
|
||||
`,
|
||||
|
||||
header: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${cssVar.colorSplit};
|
||||
padding-inline: ${cssVar.paddingLG} !important;
|
||||
height: calc(${cssVar.controlHeightLG} * 1.2);
|
||||
line-height: calc(${cssVar.controlHeightLG} * 1.2);
|
||||
`,
|
||||
|
||||
headerDark: css`
|
||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||
`,
|
||||
|
||||
avatar: css`
|
||||
width: ${cssVar.controlHeight};
|
||||
height: ${cssVar.controlHeight};
|
||||
border-radius: 100%;
|
||||
background: rgba(240, 240, 240, 0.75);
|
||||
background-size: cover;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
|
||||
`,
|
||||
|
||||
avatarDark: css`
|
||||
background: rgba(200, 200, 200, 0.3);
|
||||
`,
|
||||
|
||||
logo: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: ${cssVar.padding};
|
||||
|
||||
h1 {
|
||||
font-weight: 400;
|
||||
font-size: ${cssVar.fontSizeLG};
|
||||
line-height: 1.5;
|
||||
}
|
||||
`,
|
||||
|
||||
logoImg: css`
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
vertical-align: top;
|
||||
}
|
||||
`,
|
||||
|
||||
transBg: css`
|
||||
background: transparent !important;
|
||||
`,
|
||||
|
||||
form: css`
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
`,
|
||||
pos: css`
|
||||
position: absolute;
|
||||
`,
|
||||
leftTopImagePos: css`
|
||||
inset-inline-start: 0;
|
||||
top: -100px;
|
||||
height: 500px;
|
||||
`,
|
||||
rightBottomPos: css`
|
||||
inset-inline-end: 0;
|
||||
bottom: -100px;
|
||||
height: 287px;
|
||||
`,
|
||||
leftTopImage: css`
|
||||
inset-inline-start: 50%;
|
||||
transform: translate3d(-900px, 0, 0);
|
||||
top: -100px;
|
||||
height: 500px;
|
||||
`,
|
||||
rightBottomImage: css`
|
||||
inset-inline-end: 50%;
|
||||
transform: translate3d(750px, 0, 0);
|
||||
bottom: -100px;
|
||||
height: 287px;
|
||||
`,
|
||||
motion: css`
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
`,
|
||||
op1: css`
|
||||
opacity: 1;
|
||||
`,
|
||||
op0: css`
|
||||
opacity: 0;
|
||||
`,
|
||||
carousel,
|
||||
};
|
||||
});
|
||||
|
||||
// ========================== Menu Config ==========================
|
||||
const subMenuItems = [
|
||||
{
|
||||
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: Color, isLight?: boolean) {
|
||||
if (!isLight) {
|
||||
return '#FFF';
|
||||
}
|
||||
|
||||
const color = generateColor(colorPrimary);
|
||||
const closestColor = getClosetColor(colorPrimary);
|
||||
|
||||
switch (closestColor) {
|
||||
case DEFAULT_COLOR:
|
||||
case PINK_COLOR:
|
||||
case '#F2BD27':
|
||||
return undefined;
|
||||
|
||||
case '#5A54F9':
|
||||
case '#E0282E':
|
||||
return '#FFF';
|
||||
|
||||
default:
|
||||
return color.toHsb().b < 0.7 ? '#FFF' : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface ThemeData {
|
||||
themeType: THEME;
|
||||
colorPrimary: Color;
|
||||
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,
|
||||
},
|
||||
v4: {
|
||||
...defaultTheme.token,
|
||||
},
|
||||
};
|
||||
|
||||
const normalize = (value: number) => value / 255;
|
||||
|
||||
function rgbToColorMatrix(color: string) {
|
||||
const rgb = new FastColor(color).toRgb();
|
||||
const { r, g, b } = rgb;
|
||||
|
||||
const invertValue = normalize(r) * 100;
|
||||
const sepiaValue = 100;
|
||||
const saturateValue = Math.max(normalize(r), normalize(g), normalize(b)) * 10000;
|
||||
const hueRotateValue =
|
||||
((Math.atan2(
|
||||
Math.sqrt(3) * (normalize(g) - normalize(b)),
|
||||
2 * normalize(r) - normalize(g) - normalize(b),
|
||||
) *
|
||||
180) /
|
||||
Math.PI +
|
||||
360) %
|
||||
360;
|
||||
|
||||
return `invert(${invertValue}%) sepia(${sepiaValue}%) saturate(${saturateValue}%) hue-rotate(${hueRotateValue}deg)`;
|
||||
}
|
||||
|
||||
const Theme: React.FC = () => {
|
||||
const [locale, lang] = useLocale(locales);
|
||||
const isZhCN = lang === 'cn';
|
||||
const { search } = useLocation();
|
||||
|
||||
const [themeData, setThemeData] = React.useState<ThemeData>(ThemeDefault);
|
||||
|
||||
const onThemeChange = (_: Partial<ThemeData>, nextThemeData: ThemeData) => {
|
||||
React.startTransition(() => {
|
||||
setThemeData({ ...ThemesInfo[nextThemeData.themeType], ...nextThemeData });
|
||||
});
|
||||
};
|
||||
|
||||
const { compact, themeType, colorPrimary, ...themeToken } = themeData;
|
||||
const isLight = themeType !== 'dark';
|
||||
const [form] = Form.useForm();
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
const colorPrimaryValue = React.useMemo(
|
||||
() => (typeof colorPrimary === 'string' ? colorPrimary : colorPrimary.toHexString()),
|
||||
[colorPrimary],
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (themeType === 'v4') {
|
||||
algorithms.push(defaultAlgorithm);
|
||||
}
|
||||
|
||||
return algorithms;
|
||||
}, [isLight, compact, themeType]);
|
||||
|
||||
// ================================ Themes ================================
|
||||
React.useEffect(() => {
|
||||
const mergedData = {
|
||||
...ThemeDefault,
|
||||
themeType,
|
||||
...ThemesInfo[themeType],
|
||||
};
|
||||
setThemeData(mergedData);
|
||||
form.setFieldsValue(mergedData);
|
||||
}, [form, themeType]);
|
||||
|
||||
const isDark = React.use(DarkContext);
|
||||
|
||||
React.useEffect(() => {
|
||||
onThemeChange({}, { ...themeData, themeType: isDark ? 'dark' : 'default' });
|
||||
}, [isDark]);
|
||||
|
||||
// ================================ Tokens ================================
|
||||
const closestColor = getClosetColor(colorPrimaryValue);
|
||||
|
||||
const [backgroundColor, avatarColor] = React.useMemo(() => {
|
||||
let bgColor = 'transparent';
|
||||
|
||||
const mapToken = theme.defaultAlgorithm({
|
||||
...theme.defaultConfig.token,
|
||||
colorPrimary: colorPrimaryValue,
|
||||
});
|
||||
|
||||
if (themeType === 'dark') {
|
||||
bgColor = '#393F4A';
|
||||
} else if (closestColor === DEFAULT_COLOR) {
|
||||
bgColor = '#F5F8FF';
|
||||
} else {
|
||||
bgColor = mapToken.colorPrimaryHover;
|
||||
}
|
||||
|
||||
return [bgColor, mapToken.colorPrimaryBgHover];
|
||||
}, [themeType, closestColor, colorPrimaryValue]);
|
||||
|
||||
const logoColor = React.useMemo(() => {
|
||||
const hsb = generateColor(colorPrimaryValue).toHsb();
|
||||
hsb.b = Math.min(hsb.b, 0.7);
|
||||
return generateColor(hsb).toHexString();
|
||||
}, [colorPrimaryValue]);
|
||||
|
||||
const memoTheme = React.useMemo<ThemeConfig>(
|
||||
() => ({
|
||||
token: { ...themeToken, colorPrimary: colorPrimaryValue },
|
||||
algorithm: algorithmFn,
|
||||
components: {
|
||||
Layout: isLight ? { headerBg: 'transparent', bodyBg: 'transparent' } : {},
|
||||
Menu: isLight
|
||||
? { itemBg: 'transparent', subMenuItemBg: 'transparent', activeBarBorderWidth: 0 }
|
||||
: {},
|
||||
...(themeType === 'v4' ? defaultTheme.components : {}),
|
||||
},
|
||||
}),
|
||||
[themeToken, colorPrimaryValue, algorithmFn, isLight, themeType],
|
||||
);
|
||||
|
||||
// ================================ Render ================================
|
||||
const themeNode = (
|
||||
<ConfigProvider theme={memoTheme}>
|
||||
<TokenChecker />
|
||||
<div
|
||||
className={clsx(styles.demo, {
|
||||
[styles.otherDemo]: isLight && closestColor !== DEFAULT_COLOR && styles.otherDemo,
|
||||
[styles.darkDemo]: !isLight,
|
||||
})}
|
||||
style={{ borderRadius: themeData.borderRadius }}
|
||||
>
|
||||
<Layout className={styles.transBg}>
|
||||
<Header className={clsx(styles.header, styles.transBg, !isLight && styles.headerDark)}>
|
||||
{/* Logo */}
|
||||
<div className={styles.logo}>
|
||||
<div className={styles.logoImg}>
|
||||
<img
|
||||
draggable={false}
|
||||
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
|
||||
style={{
|
||||
filter:
|
||||
closestColor === DEFAULT_COLOR ? undefined : rgbToColorMatrix(logoColor),
|
||||
}}
|
||||
alt="antd logo"
|
||||
/>
|
||||
</div>
|
||||
<h1>Ant Design</h1>
|
||||
</div>
|
||||
<Flex className={styles.menu} gap="middle">
|
||||
<BellOutlined />
|
||||
<QuestionCircleOutlined />
|
||||
<div
|
||||
className={clsx(styles.avatar, { [styles.avatarDark]: themeType === 'dark' })}
|
||||
style={{
|
||||
backgroundColor: avatarColor,
|
||||
backgroundImage: `url(${getAvatarURL(closestColor)})`,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Header>
|
||||
<Layout className={styles.transBg} hasSider>
|
||||
<Sider className={clsx(styles.transBg)} width={200}>
|
||||
<Menu
|
||||
mode="inline"
|
||||
className={clsx(styles.transBg)}
|
||||
selectedKeys={['Themes']}
|
||||
openKeys={['Design']}
|
||||
style={{ height: '100%', borderInlineEnd: 0 }}
|
||||
items={sideMenuItems}
|
||||
expandIcon={false}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout className={styles.transBg} style={{ padding: '0 24px 24px' }}>
|
||||
<Breadcrumb
|
||||
style={{ margin: '16px 0' }}
|
||||
items={[
|
||||
{ title: <HomeOutlined /> },
|
||||
{ title: 'Design', menu: { items: subMenuItems } },
|
||||
{ title: 'Themes' },
|
||||
]}
|
||||
/>
|
||||
<Content>
|
||||
<Typography.Title level={2}>{locale.customizeTheme}</Typography.Title>
|
||||
<Card
|
||||
title={locale.myTheme}
|
||||
extra={
|
||||
<Flex gap="small">
|
||||
<LinkButton to={getLocalizedPathname('/theme-editor', isZhCN, search)}>
|
||||
{locale.toDef}
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
type="primary"
|
||||
to={getLocalizedPathname('/docs/react/customize-theme', isZhCN, search)}
|
||||
>
|
||||
{locale.toUse}
|
||||
</LinkButton>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={themeData}
|
||||
onValuesChange={onThemeChange}
|
||||
labelCol={{ span: 3 }}
|
||||
wrapperCol={{ span: 21 }}
|
||||
className={styles.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" htmlFor="compact_default">
|
||||
<Radio.Group
|
||||
options={[
|
||||
{ label: locale.default, value: 'default', id: 'compact_default' },
|
||||
{ label: locale.compact, value: 'compact' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
<MobileCarousel title={locale.themeTitle} description={locale.themeDesc} id="flexible" />
|
||||
) : (
|
||||
<Group
|
||||
title={locale.themeTitle}
|
||||
titleColor={getTitleColor(colorPrimaryValue, isLight)}
|
||||
description={locale.themeDesc}
|
||||
id="flexible"
|
||||
background={backgroundColor}
|
||||
decoration={
|
||||
// =========================== Theme Background ===========================
|
||||
<>
|
||||
{/* >>>>>> Default <<<<<< */}
|
||||
<div
|
||||
className={clsx(
|
||||
styles.motion,
|
||||
isLight && closestColor === DEFAULT_COLOR ? styles.op1 : styles.op0,
|
||||
)}
|
||||
>
|
||||
{/* Image Left Top */}
|
||||
<img
|
||||
draggable={false}
|
||||
className={clsx(styles.pos, styles.leftTopImage)}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/bd71b0c6-f93a-4e52-9c8a-f01a9b8fe22b.svg"
|
||||
alt="image-left-top"
|
||||
/>
|
||||
{/* Image Right Bottom */}
|
||||
<img
|
||||
draggable={false}
|
||||
className={clsx(styles.pos, styles.rightBottomImage)}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/84ad805a-74cb-4916-b7ba-9cdc2bdec23a.svg"
|
||||
alt="image-right-bottom"
|
||||
/>
|
||||
</div>
|
||||
{/* >>>>>> Dark <<<<<< */}
|
||||
<div className={clsx(styles.motion, !isLight || !closestColor ? styles.op1 : styles.op0)}>
|
||||
{/* Image Left Top */}
|
||||
<img
|
||||
draggable={false}
|
||||
className={clsx(styles.pos, styles.leftTopImagePos)}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/a213184a-f212-4afb-beec-1e8b36bb4b8a.svg"
|
||||
alt="image-left-top"
|
||||
/>
|
||||
{/* Image Right Bottom */}
|
||||
<img
|
||||
draggable={false}
|
||||
className={clsx(styles.pos, styles.rightBottomPos)}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/bb74a2fb-bff1-4d0d-8c2d-2ade0cd9bb0d.svg"
|
||||
alt="image-right-bottom"
|
||||
/>
|
||||
</div>
|
||||
{/* >>>>>> Background Image <<<<<< */}
|
||||
<BackgroundImage isLight={isLight} colorPrimary={colorPrimaryValue} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
{themeNode}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default Theme;
|
||||
@@ -1,144 +0,0 @@
|
||||
import { css } from 'antd-style';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export interface Author {
|
||||
avatar: string;
|
||||
href: string;
|
||||
type: 'design' | 'develop';
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
title: string;
|
||||
href: string;
|
||||
date: string;
|
||||
type: 'design' | 'develop';
|
||||
author: Author['name'];
|
||||
}
|
||||
|
||||
export interface Recommendation {
|
||||
title?: string;
|
||||
img?: string;
|
||||
href?: string;
|
||||
popularize?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
type SourceType = 'zhihu' | 'yuque';
|
||||
|
||||
export interface Extra {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
img: string;
|
||||
source: SourceType;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface Icon {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export type Articles = {
|
||||
cn: Article[];
|
||||
en: Article[];
|
||||
};
|
||||
|
||||
export type Authors = Author[];
|
||||
|
||||
export type Recommendations = {
|
||||
cn: Recommendation[];
|
||||
en: Recommendation[];
|
||||
};
|
||||
|
||||
export type Extras = {
|
||||
cn: Extra[];
|
||||
en: Extra[];
|
||||
};
|
||||
|
||||
export type Icons = Icon[];
|
||||
|
||||
export type HeadingBanner = {
|
||||
[key in 'cn' | 'en']: {
|
||||
title?: string;
|
||||
href?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type SiteData = {
|
||||
headingBanner: HeadingBanner;
|
||||
articles: Articles;
|
||||
authors: Authors;
|
||||
recommendations: Recommendations;
|
||||
extras: Extras;
|
||||
icons: Icons;
|
||||
};
|
||||
|
||||
export function preLoad(list: string[]) {
|
||||
if (typeof window !== 'undefined') {
|
||||
// 图处预加载;
|
||||
const div = document.createElement('div');
|
||||
div.style.display = 'none';
|
||||
document.body.appendChild(div);
|
||||
list.forEach((src) => {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
div.appendChild(img);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Banner 硬编码,以防止页面闪烁问题
|
||||
* 返回 null 表示不显示
|
||||
*/
|
||||
export const getBannerData = (): null | {
|
||||
title: string;
|
||||
href: string;
|
||||
} => {
|
||||
// return {
|
||||
// title: 'See Conf 2025 震撼来袭 - 探索 AI 时代的用户体验与工程实践',
|
||||
// href: 'https://seeconf.antfin.com/',
|
||||
// };
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useAntdSiteConfig = () => {
|
||||
const { data, error, isLoading } = useSWR<Partial<SiteData>, Error>(
|
||||
`https://render.alipay.com/p/h5data/antd4-config_website-h5data.json`,
|
||||
(url: string) => fetch(url).then((res) => res.json()),
|
||||
{
|
||||
suspense: false,
|
||||
// revalidateOnMount: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
return { data, error, isLoading };
|
||||
};
|
||||
|
||||
export const getCarouselStyle = () => ({
|
||||
carousel: css`
|
||||
.slick-dots.slick-dots-bottom {
|
||||
bottom: -22px;
|
||||
li {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #e1eeff;
|
||||
border-radius: 50%;
|
||||
button {
|
||||
height: 6px;
|
||||
background: #e1eeff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
&.slick-active {
|
||||
background: #4b9cff;
|
||||
button {
|
||||
background: #4b9cff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
import useLocale from '../../hooks/useLocale';
|
||||
import { DarkContext } from './../../hooks/useDark';
|
||||
import BannerRecommends from './components/BannerRecommends';
|
||||
import Group from './components/Group';
|
||||
import PreviewBanner from './components/PreviewBanner';
|
||||
|
||||
const ComponentsList = React.lazy(() => import('./components/ComponentsList'));
|
||||
const DesignFramework = React.lazy(() => import('./components/DesignFramework'));
|
||||
const Theme = React.lazy(() => import('./components/Theme'));
|
||||
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
image: css`
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: -50px;
|
||||
height: 160px;
|
||||
`,
|
||||
}));
|
||||
|
||||
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: React.FC = () => {
|
||||
const [locale] = useLocale(locales);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const isDark = React.use(DarkContext);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<PreviewBanner>
|
||||
<BannerRecommends />
|
||||
</PreviewBanner>
|
||||
|
||||
<div>
|
||||
{/* 定制主题 */}
|
||||
<ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
|
||||
<Suspense fallback={null}>
|
||||
<Theme />
|
||||
</Suspense>
|
||||
</ConfigProvider>
|
||||
|
||||
{/* 组件列表 */}
|
||||
<Group
|
||||
background={token.colorBgElevated}
|
||||
collapse
|
||||
title={locale.assetsTitle}
|
||||
description={locale.assetsDesc}
|
||||
id="design"
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<ComponentsList />
|
||||
</Suspense>
|
||||
</Group>
|
||||
|
||||
{/* 设计语言 */}
|
||||
<Group
|
||||
title={locale.designTitle}
|
||||
description={locale.designDesc}
|
||||
background={isDark ? '#393F4A' : '#F5F8FF'}
|
||||
decoration={
|
||||
<img
|
||||
draggable={false}
|
||||
className={classNames.image}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/ba37a413-28e6-4be4-b1c5-01be1a0ebb1c.svg"
|
||||
alt="bg"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<DesignFramework />
|
||||
</Suspense>
|
||||
</Group>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Homepage;
|
||||
@@ -1,3 +0,0 @@
|
||||
import ThemeEditor from '../theme-editor';
|
||||
|
||||
export default ThemeEditor;
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { App, Button, ConfigProvider, Skeleton, version } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { enUS, zhCN } from 'antd-token-previewer';
|
||||
import type { ThemeConfig } from 'antd/es/config-provider/context';
|
||||
import { Helmet } from 'dumi';
|
||||
|
||||
import useLocale from '../../hooks/useLocale';
|
||||
|
||||
const ThemeEditor = React.lazy(() => import('antd-token-previewer/lib/ThemeEditor'));
|
||||
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
editor: css`
|
||||
svg,
|
||||
img {
|
||||
display: inline;
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
title: '主题编辑器',
|
||||
save: '保存',
|
||||
edit: '编辑',
|
||||
export: '导出',
|
||||
editModelTitle: '编辑主题配置',
|
||||
editJsonContentTypeError: '主题 JSON 格式错误',
|
||||
editSuccessfully: '编辑成功',
|
||||
saveSuccessfully: '保存成功',
|
||||
initialEditor: '正在初始化编辑器...',
|
||||
},
|
||||
en: {
|
||||
title: 'Theme Editor',
|
||||
save: 'Save',
|
||||
edit: 'Edit',
|
||||
export: 'Export',
|
||||
editModelTitle: 'edit Theme Config',
|
||||
editJsonContentTypeError: 'The theme of the JSON format is incorrect',
|
||||
editSuccessfully: 'Edited successfully',
|
||||
saveSuccessfully: 'Saved successfully',
|
||||
initialEditor: 'Initializing Editor...',
|
||||
},
|
||||
};
|
||||
|
||||
const [antdMajor] = version.split('.');
|
||||
const ANT_DESIGN_V5_THEME_EDITOR_THEME = `ant-design-v${antdMajor}-theme-editor-theme`;
|
||||
|
||||
const CustomTheme: React.FC = () => {
|
||||
const { message } = App.useApp();
|
||||
const [locale, lang] = useLocale(locales);
|
||||
|
||||
const [theme, setTheme] = React.useState<ThemeConfig>(() => {
|
||||
try {
|
||||
const storedConfig = localStorage.getItem(ANT_DESIGN_V5_THEME_EDITOR_THEME);
|
||||
return storedConfig ? JSON.parse(storedConfig) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
localStorage.setItem(ANT_DESIGN_V5_THEME_EDITOR_THEME, JSON.stringify(theme));
|
||||
message.success(locale.saveSuccessfully);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{`${locale.title} - Ant Design`}</title>
|
||||
<meta property="og:title" content={`${locale.title} - Ant Design`} />
|
||||
</Helmet>
|
||||
<Suspense fallback={<Skeleton style={{ margin: 24 }} />}>
|
||||
<ConfigProvider theme={{ inherit: false }}>
|
||||
<ThemeEditor
|
||||
advanced
|
||||
hideAdvancedSwitcher
|
||||
theme={{ name: 'Custom Theme', key: 'test', config: theme }}
|
||||
style={{ height: 'calc(100vh - 64px)' }}
|
||||
className={classNames.editor}
|
||||
onThemeChange={(newTheme) => {
|
||||
setTheme(newTheme.config);
|
||||
}}
|
||||
locale={lang === 'cn' ? zhCN : enUS}
|
||||
actions={
|
||||
<Button type="primary" onClick={handleSave}>
|
||||
{locale.save}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomTheme;
|
||||
@@ -1,106 +0,0 @@
|
||||
import assert from 'assert';
|
||||
import type { HastRoot, UnifiedTransformer } from 'dumi';
|
||||
import { unistUtilVisit } from 'dumi';
|
||||
|
||||
/**
|
||||
* plugin for modify hast tree when docs compiling
|
||||
*/
|
||||
function rehypeAntd(): UnifiedTransformer<HastRoot> {
|
||||
return (tree, vFile) => {
|
||||
const { filename } = vFile.data.frontmatter as any;
|
||||
|
||||
unistUtilVisit.visit(tree, 'element', (node, i, parent) => {
|
||||
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 ${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;
|
||||
}, []),
|
||||
),
|
||||
},
|
||||
];
|
||||
} else if (
|
||||
node.type === 'element' &&
|
||||
node.tagName === 'Table' &&
|
||||
/^components/.test(filename)
|
||||
) {
|
||||
if (!node.properties) {
|
||||
return;
|
||||
}
|
||||
node.properties.className ??= [];
|
||||
(node.properties.className as string[]).push('component-api-table');
|
||||
} else if (node.type === 'element' && (node.tagName === 'Link' || node.tagName === 'a')) {
|
||||
const { tagName } = node;
|
||||
node.properties!.sourceType = tagName;
|
||||
node.tagName = 'LocaleLink';
|
||||
} else if (node.type === 'element' && node.tagName === 'video') {
|
||||
node.tagName = 'VideoPlayer';
|
||||
} else if (node.tagName === 'SourceCode') {
|
||||
const { lang } = node.properties!;
|
||||
|
||||
if (typeof lang === 'string' && lang.startsWith('sandpack')) {
|
||||
const code = (node.children[0] as any).value as string;
|
||||
const configRegx = /^const sandpackConfig = ([\s\S]*?});/;
|
||||
const [configString] = code.match(configRegx) || [];
|
||||
/* biome-ignore lint/security/noGlobalEval: used in documentation */ /* eslint-disable-next-line no-eval */
|
||||
const config = configString && eval(`(${configString.replace(configRegx, '$1')})`);
|
||||
Object.keys(config || {}).forEach((key) => {
|
||||
if (typeof config[key] === 'object') {
|
||||
config[key] = JSON.stringify(config[key]);
|
||||
}
|
||||
});
|
||||
|
||||
parent!.children.splice(i!, 1, {
|
||||
type: 'element',
|
||||
tagName: 'Sandpack',
|
||||
properties: {
|
||||
...config,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: code.replace(configRegx, '').trim(),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default rehypeAntd;
|
||||
@@ -1,156 +0,0 @@
|
||||
import type { UnifiedTransformer } from 'dumi';
|
||||
import { unistUtilVisit } from 'dumi';
|
||||
import set from 'lodash/set';
|
||||
import semver from 'semver';
|
||||
|
||||
let hastToString: typeof import('hast-util-to-string').toString;
|
||||
|
||||
// workaround to import pure esm module
|
||||
(async () => {
|
||||
({ toString: hastToString } = await import('hast-util-to-string'));
|
||||
})();
|
||||
|
||||
function isValidStrictVer(ver: string): boolean {
|
||||
if (!semver.valid(ver)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = ver.split('.');
|
||||
if (parts.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parts.every((part) => /^\d+$/.test(part));
|
||||
}
|
||||
|
||||
function isValidDate(dateStr: string): boolean {
|
||||
// (YYYY-MM-DD)
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(dateStr);
|
||||
}
|
||||
|
||||
const COMPONENT_NAME = 'RefinedChangelog';
|
||||
|
||||
function rehypeChangelog(): UnifiedTransformer<any> {
|
||||
return (tree, vFile) => {
|
||||
const { filename } = vFile.data.frontmatter as any;
|
||||
|
||||
// 只处理 changelog 文件
|
||||
if (!/^changelog\.\S+\.md$/i.test(filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodesToWrap: { parent: any; startIdx: number }[] = [];
|
||||
const WRAPPER_FLAG = 'data-changelog-wrapped'; // 包裹容器唯一标识
|
||||
|
||||
function checkLogSegment(node: any, strict = true) {
|
||||
if (node && node.type === 'element' && node.tagName === 'h2') {
|
||||
if (strict) {
|
||||
const ver = hastToString(node);
|
||||
return isValidStrictVer(ver) && semver.major(ver) >= 5;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unistUtilVisit.visit(tree, 'element', (node, idx, parent) => {
|
||||
if (node.properties?.[WRAPPER_FLAG]) {
|
||||
return unistUtilVisit.SKIP;
|
||||
}
|
||||
|
||||
if (idx !== undefined && parent && checkLogSegment(node)) {
|
||||
nodesToWrap.push({ parent, startIdx: idx! });
|
||||
}
|
||||
});
|
||||
|
||||
const totalNodesToWrap = nodesToWrap.length;
|
||||
for (let i = totalNodesToWrap - 1; i >= 0; i--) {
|
||||
const { parent, startIdx } = nodesToWrap[i];
|
||||
|
||||
let endIdx = -1;
|
||||
const isEndOfWrap = i === totalNodesToWrap - 1;
|
||||
for (let j = startIdx + 1; j < parent.children.length; j++) {
|
||||
const nextNode = parent.children[j];
|
||||
if (
|
||||
(isEndOfWrap && checkLogSegment(nextNode, false)) || // 日志页通常还存在历史 major 版本
|
||||
nextNode.properties?.[WRAPPER_FLAG] || // 已经被处理
|
||||
checkLogSegment(nextNode) // 下一段日志
|
||||
) {
|
||||
endIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (endIdx === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Version
|
||||
const heading = parent.children[startIdx];
|
||||
|
||||
// Find Date
|
||||
let dateIdx = -1;
|
||||
for (let j = startIdx + 1; j < endIdx; j++) {
|
||||
const node = parent.children[j];
|
||||
if (node.type === 'element' && isValidDate(hastToString(node))) {
|
||||
dateIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dateIdx === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect list nodes between dateIdx and endIdx
|
||||
const version = hastToString(heading);
|
||||
const date = parent.children[dateIdx];
|
||||
const dateStr = hastToString(date);
|
||||
const details = parent.children.slice(dateIdx + 1, endIdx);
|
||||
|
||||
const headingWrap = {
|
||||
type: 'element',
|
||||
tagName: `${COMPONENT_NAME}.Version`,
|
||||
children: [set(heading, 'properties.className', 'changelog-version')],
|
||||
};
|
||||
|
||||
const dateWrap = {
|
||||
type: 'element',
|
||||
tagName: `${COMPONENT_NAME}.Date`,
|
||||
children: [set(date, 'properties.className', 'changelog-date')],
|
||||
};
|
||||
|
||||
const detailWrap = {
|
||||
type: 'element',
|
||||
tagName: `${COMPONENT_NAME}.Details`,
|
||||
properties: {
|
||||
className: 'changelog-details',
|
||||
},
|
||||
children: details,
|
||||
};
|
||||
|
||||
const wrapper = {
|
||||
type: 'element',
|
||||
tagName: COMPONENT_NAME,
|
||||
properties: {
|
||||
[WRAPPER_FLAG]: true,
|
||||
},
|
||||
JSXAttributes: [
|
||||
{
|
||||
type: 'JSXAttribute',
|
||||
name: 'version',
|
||||
value: JSON.stringify(version),
|
||||
},
|
||||
{
|
||||
type: 'JSXAttribute',
|
||||
name: 'date',
|
||||
value: JSON.stringify(dateStr),
|
||||
},
|
||||
],
|
||||
children: [headingWrap, dateWrap, detailWrap],
|
||||
};
|
||||
|
||||
parent.children.splice(startIdx, endIdx - startIdx, wrapper);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default rehypeChangelog;
|
||||
@@ -1,89 +0,0 @@
|
||||
import { unistUtilVisit } from 'dumi';
|
||||
import type { UnifiedTransformer } from 'dumi';
|
||||
|
||||
let toSlug: typeof import('github-slugger').slug;
|
||||
|
||||
// workaround to import pure esm module
|
||||
(async () => {
|
||||
({ slug: toSlug } = await import('github-slugger'));
|
||||
})();
|
||||
|
||||
const isNil = (value: any) => value == null;
|
||||
|
||||
const toArr = <T>(value: T | T[]) => {
|
||||
if (isNil(value)) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(value) ? value : [value];
|
||||
};
|
||||
|
||||
const patch = (context: Record<string, any>, key: string, value: any) => {
|
||||
if (!context[key]) {
|
||||
context[key] = value;
|
||||
}
|
||||
return context[key];
|
||||
};
|
||||
|
||||
interface Options {
|
||||
level?: number;
|
||||
}
|
||||
|
||||
const remarkAnchor = (opt: Options = {}): UnifiedTransformer<any> => {
|
||||
// https://regex101.com/r/WDjkK0/1
|
||||
const RE = /\s*\{#([^}]+)\}$/;
|
||||
|
||||
const realOpt = {
|
||||
level: [1, 2, 3, 4, 5, 6],
|
||||
...opt,
|
||||
};
|
||||
|
||||
return function transformer(tree) {
|
||||
const ids = new Set();
|
||||
|
||||
unistUtilVisit.visit(tree, 'heading', (node) => {
|
||||
if (!toArr(realOpt.level).includes(node.depth)) {
|
||||
return unistUtilVisit.CONTINUE;
|
||||
}
|
||||
|
||||
const lastIndex = node.children.length - 1;
|
||||
const lastChild = node.children[lastIndex];
|
||||
|
||||
if (lastChild?.type === 'text') {
|
||||
const text = lastChild.value;
|
||||
const match = text.match(RE);
|
||||
if (match) {
|
||||
const id = match[1];
|
||||
if (id !== toSlug(id)) {
|
||||
throw new Error(
|
||||
`Expected header ID to be a valid slug. You specified: {#${id}}. Replace it with: {#${toSlug(id)}}`,
|
||||
);
|
||||
}
|
||||
|
||||
node.data ??= {};
|
||||
node.data.hProperties = { ...node.data.hProperties, id };
|
||||
|
||||
lastChild.value = text.replace(RE, '');
|
||||
|
||||
if (lastChild.value === '') {
|
||||
node.children.pop();
|
||||
}
|
||||
if (ids.has(id)) {
|
||||
throw new Error(`Cannot have a duplicate header with id "${id}" on the page.
|
||||
Rename the section or give it an explicit unique ID. For example: #### Arguments {#setstate-arguments}`);
|
||||
}
|
||||
|
||||
ids.add(id);
|
||||
|
||||
const data = patch(node, 'data', {});
|
||||
patch(data, 'id', id);
|
||||
patch(data, 'htmlAttributes', {});
|
||||
patch(data, 'hProperties', {});
|
||||
patch(data.htmlAttributes, 'id', id);
|
||||
patch(data.hProperties, 'id', id);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export default remarkAnchor;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { unistUtilVisit } from 'dumi';
|
||||
import type { UnifiedTransformer } from 'dumi';
|
||||
|
||||
function remarkMeta(): UnifiedTransformer<any> {
|
||||
return (tree, vFile) => {
|
||||
// read frontmatter
|
||||
unistUtilVisit.visit(tree, 'yaml', (node) => {
|
||||
if (!/(^|[\n\r])description:/.test(node.value)) {
|
||||
(vFile.data.frontmatter as any).__autoDescription = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default remarkMeta;
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// https://clarity.microsoft.com
|
||||
(function (c, l, a, r, i, t, y) {
|
||||
c[a] =
|
||||
c[a] ||
|
||||
function () {
|
||||
(c[a].q = c[a].q || []).push(arguments);
|
||||
};
|
||||
t = l.createElement(r);
|
||||
t.async = 1;
|
||||
t.src = 'https://www.clarity.ms/tag/' + i;
|
||||
y = l.getElementsByTagName(r)[0];
|
||||
y.parentNode.insertBefore(t, y);
|
||||
})(window, document, 'clarity', 'script', 'lyia7jfwui');
|
||||
@@ -1,273 +0,0 @@
|
||||
(function createMirrorModal() {
|
||||
const SIGN = Symbol.for('antd.mirror-notify');
|
||||
const always = window.localStorage.getItem('DEBUG') === 'antd';
|
||||
const officialChinaMirror = 'https://ant-design.antgroup.com?utm_source=mirror-notify';
|
||||
|
||||
const enabledCondition = [
|
||||
// Check if the browser language is Chinese
|
||||
navigator.languages.includes('zh') || navigator.languages.includes('zh-CN'),
|
||||
// Check if the URL path ends with -cn
|
||||
/-cn\/?$/.test(window.location.pathname),
|
||||
// chinese mirror URL
|
||||
!['ant-design.gitee.io', new URL(officialChinaMirror).hostname].includes(
|
||||
window.location.hostname,
|
||||
),
|
||||
// PR review URL
|
||||
!window.location.host.includes('surge'),
|
||||
// development mode
|
||||
!['127.0.0.1', 'localhost'].includes(window.location.hostname),
|
||||
];
|
||||
|
||||
const isEnabled = always || enabledCondition.every(Boolean);
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefixCls = 'antd-mirror-notify';
|
||||
const primaryColor = '#1677ff';
|
||||
|
||||
function insertCss() {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translate3d(100%, 0, 0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.${prefixCls} {
|
||||
position: fixed;
|
||||
inset-inline-end: 12px;
|
||||
inset-block-start: 12px;
|
||||
z-index: 9999;
|
||||
width: 360px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
animation: slideInRight 0.3s ease-in-out;
|
||||
}
|
||||
.${prefixCls}-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.${prefixCls}-content a {
|
||||
color: ${primaryColor};
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.${prefixCls}-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-block-end: 8px;
|
||||
}
|
||||
.${prefixCls}-message {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
line-height: 1.57;
|
||||
}
|
||||
.${prefixCls}-footer {
|
||||
display: none;
|
||||
margin-block-start: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.${prefixCls}-progress {
|
||||
position: relative;
|
||||
inset-inline-end: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.${prefixCls}-progress::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: var(--progress, 0%);
|
||||
background-color: ${primaryColor};
|
||||
transition: width 0.05s linear; /* Adjusted for smoother animation matching refreshRate */
|
||||
}
|
||||
.${prefixCls}-close {
|
||||
all: unset;
|
||||
position: absolute;
|
||||
inset-inline-end: 2px;
|
||||
inset-block-start: 2px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.${prefixCls}-close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.${prefixCls}-action {
|
||||
all: unset;
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
background-color: ${primaryColor};
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.append(style);
|
||||
}
|
||||
|
||||
function createNotification() {
|
||||
insertCss();
|
||||
|
||||
const notify = document.createElement('div');
|
||||
notify.className = `${prefixCls} slideInRight`;
|
||||
notify.innerHTML = `
|
||||
<div class="${prefixCls}-content">
|
||||
<div class="${prefixCls}-title">🇨🇳 访问不畅?试试国内镜像</div>
|
||||
<div class="${prefixCls}-message">
|
||||
国内镜像站点可以帮助您更快地访问文档和资源。<br>
|
||||
请尝试访问 <a class="${prefixCls}-link" href="${officialChinaMirror}">国内镜像站点</a>,以获得更好的体验。
|
||||
</div>
|
||||
<div class="${prefixCls}-footer">
|
||||
<button class="${prefixCls}-action ${prefixCls}-link">🚀 立即前往</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="${prefixCls}-close">X</button>
|
||||
<div class="${prefixCls}-progress" style="--progress: 100%;"></div>
|
||||
`;
|
||||
document.body.appendChild(notify);
|
||||
|
||||
notify.querySelector(`.${prefixCls}-close`).addEventListener('click', () => {
|
||||
removeNotify();
|
||||
});
|
||||
|
||||
const goToChinaMirror = (event) => {
|
||||
event.preventDefault();
|
||||
if (window.gtag) {
|
||||
window.gtag('event', '点击', {
|
||||
event_category: '前往国内镜像',
|
||||
event_label: officialChinaMirror,
|
||||
});
|
||||
}
|
||||
window.location.href = officialChinaMirror;
|
||||
removeNotify();
|
||||
};
|
||||
|
||||
notify.querySelectorAll(`.${prefixCls}-link`).forEach((link) => {
|
||||
link.addEventListener('click', goToChinaMirror);
|
||||
});
|
||||
|
||||
const refreshRate = 50; // ms
|
||||
const duration = 10; // s
|
||||
const step = 100 / ((duration * 1000) / refreshRate);
|
||||
let progressInterval = -1;
|
||||
|
||||
function removeNotify() {
|
||||
clearInterval(progressInterval);
|
||||
notify.remove();
|
||||
}
|
||||
|
||||
const progressEl = notify.querySelector(`.${prefixCls}-progress`);
|
||||
let currentProgressValue = 100;
|
||||
|
||||
const progress = {
|
||||
get value() {
|
||||
return currentProgressValue;
|
||||
},
|
||||
set value(val) {
|
||||
currentProgressValue = Math.max(0, Math.min(100, val));
|
||||
progressEl.style.setProperty('--progress', `${currentProgressValue}%`);
|
||||
},
|
||||
};
|
||||
|
||||
function startProgressTimer() {
|
||||
if (progressInterval !== -1) {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
progressInterval = setInterval(() => {
|
||||
if (progress.value <= 0) {
|
||||
removeNotify();
|
||||
} else {
|
||||
progress.value -= step;
|
||||
}
|
||||
}, refreshRate);
|
||||
}
|
||||
|
||||
startProgressTimer();
|
||||
|
||||
notify.addEventListener('mouseenter', () => {
|
||||
clearInterval(progressInterval);
|
||||
});
|
||||
|
||||
notify.addEventListener('mouseleave', () => {
|
||||
startProgressTimer();
|
||||
});
|
||||
}
|
||||
|
||||
function checkMirrorAvailable(timeout = 1500) {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
let done = false;
|
||||
img.onload = () => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
resolve(true);
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
img.src = new URL('/llms.txt', officialChinaMirror).href;
|
||||
setTimeout(() => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
resolve(false);
|
||||
}
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
// 断定网络不畅阈值(秒)
|
||||
const delayDuration = 3;
|
||||
|
||||
const reactTimeoutId = setTimeout(() => {
|
||||
if (typeof window[SIGN]?.YES === 'undefined') {
|
||||
console.error(
|
||||
`antd.mirror-notify: 页面加载超过 ${delayDuration} 秒,可能是网络不畅。\n请尝试访问国内镜像站点。%c${officialChinaMirror}`,
|
||||
`color: ${primaryColor}; font-weight: bold;`,
|
||||
);
|
||||
checkMirrorAvailable().then((isFast) => {
|
||||
if (isFast) {
|
||||
createNotification();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, delayDuration * 1000);
|
||||
|
||||
// 交给 React effect 清理
|
||||
window[SIGN] = function stopMirrorNotify() {
|
||||
window[SIGN].YES = Date.now();
|
||||
clearTimeout(reactTimeoutId);
|
||||
};
|
||||
})();
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import { theme as antdTheme, ConfigProvider } from 'antd';
|
||||
import type { ThemeConfig } from 'antd';
|
||||
import type { ThemeProviderProps } from 'antd-style';
|
||||
import { ThemeProvider } from 'antd-style';
|
||||
import { updateCSS } from '@rc-component/util/lib/Dom/dynamicCSS';
|
||||
|
||||
import SiteContext from './slots/SiteContext';
|
||||
|
||||
interface NewToken {
|
||||
bannerHeight: number;
|
||||
headerHeight: number;
|
||||
menuItemBorder: number;
|
||||
mobileMaxWidth: number;
|
||||
siteMarkdownCodeBg: string;
|
||||
siteMarkdownCodeBgDark: string;
|
||||
antCls: string;
|
||||
iconCls: string;
|
||||
marginFarXS: number;
|
||||
marginFarSM: number;
|
||||
marginFar: number;
|
||||
codeFamily: string;
|
||||
anchorTop: number;
|
||||
}
|
||||
|
||||
// 通过给 antd-style 扩展 CustomToken 对象类型定义,可以为 useTheme 中增加相应的 token 对象
|
||||
declare module 'antd-style' {
|
||||
export interface CustomToken extends NewToken {}
|
||||
}
|
||||
|
||||
const headerHeight = 64;
|
||||
const bannerHeight = 38;
|
||||
|
||||
const SiteThemeProvider: React.FC<ThemeProviderProps<any>> = ({ children, theme, ...rest }) => {
|
||||
const { getPrefixCls, iconPrefixCls } = React.use(ConfigProvider.ConfigContext);
|
||||
const rootPrefixCls = getPrefixCls();
|
||||
const { token } = antdTheme.useToken();
|
||||
const { bannerVisible } = React.use(SiteContext);
|
||||
React.useEffect(() => {
|
||||
// 需要注意与 components/config-provider/demo/holderRender.tsx 配置冲突
|
||||
ConfigProvider.config({ theme: theme as ThemeConfig });
|
||||
}, [theme]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// iframe demo 生效
|
||||
if (window.parent !== window) {
|
||||
updateCSS(
|
||||
`
|
||||
[data-prefers-color='dark'] {
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
|
||||
[data-prefers-color='light'] {
|
||||
color-scheme: light !important;
|
||||
}
|
||||
`,
|
||||
'color-scheme',
|
||||
);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeProvider<NewToken>
|
||||
{...rest}
|
||||
theme={theme}
|
||||
customToken={{
|
||||
headerHeight,
|
||||
bannerHeight,
|
||||
menuItemBorder: 2,
|
||||
mobileMaxWidth: 767.99,
|
||||
siteMarkdownCodeBg: token.colorFillTertiary,
|
||||
siteMarkdownCodeBgDark: '#000',
|
||||
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`,
|
||||
anchorTop: headerHeight + token.margin + (bannerVisible ? bannerHeight : 0),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteThemeProvider;
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const APITable: React.FC = () => (
|
||||
// TODO: implement api table, depend on the new markdown data structure passed
|
||||
<>API Table</>
|
||||
);
|
||||
|
||||
export default APITable;
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as all from 'antd';
|
||||
|
||||
interface AntdProps {
|
||||
component: keyof typeof all;
|
||||
}
|
||||
|
||||
const Antd: React.FC<AntdProps> = (props) => {
|
||||
const { component, ...restProps } = props;
|
||||
const Component = (all[component] ?? React.Fragment) as React.ComponentType<any>;
|
||||
return <Component {...restProps} />;
|
||||
};
|
||||
|
||||
export default Antd;
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from 'react';
|
||||
import { SoundOutlined } from '@ant-design/icons';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => {
|
||||
return {
|
||||
playBtn: css`
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
column-gap: ${cssVar.paddingXXS};
|
||||
margin: 0;
|
||||
`,
|
||||
icon: css`
|
||||
font-size: ${cssVar.fontSizeXL};
|
||||
color: ${cssVar.colorLink};
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
&:hover {
|
||||
color: ${cssVar.colorLinkHover};
|
||||
}
|
||||
&:active {
|
||||
color: ${cssVar.colorLinkActive};
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface AudioProps {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const AudioControl: React.FC<React.PropsWithChildren<AudioProps>> = ({ id, children }) => {
|
||||
const onClick: React.MouseEventHandler<HTMLAnchorElement> = () => {
|
||||
const audio = document.querySelector<HTMLAudioElement>(`#${id}`);
|
||||
audio?.play();
|
||||
};
|
||||
return (
|
||||
<a className={styles.playBtn} onClick={onClick}>
|
||||
{children}
|
||||
<SoundOutlined className={styles.icon} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioControl;
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Tag } from 'antd';
|
||||
import type { TagProps } from 'antd';
|
||||
|
||||
// https://github.com/umijs/dumi/blob/master/src/client/theme-default/builtins/Badge/index.tsx
|
||||
interface BadgeProps extends TagProps {
|
||||
type: 'info' | 'warning' | 'error' | 'success';
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
info: 'blue',
|
||||
warning: 'orange',
|
||||
error: 'red',
|
||||
success: 'green',
|
||||
};
|
||||
|
||||
export default ({ type = 'info', ...props }: BadgeProps) => (
|
||||
<Tag
|
||||
variant="filled"
|
||||
color={colorMap[type]}
|
||||
{...props}
|
||||
style={{ verticalAlign: 'top', ...props.style }}
|
||||
/>
|
||||
);
|
||||
@@ -1,69 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { FastColor } from '@ant-design/fast-color';
|
||||
import type { ColorInput } from '@ant-design/fast-color';
|
||||
import { Popover } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar, token }) => ({
|
||||
codeSpan: css`
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 0.9em;
|
||||
background: ${token.siteMarkdownCodeBg};
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
font-family: monospace;
|
||||
`,
|
||||
dot: css`
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-inline-end: ${cssVar.marginXXS};
|
||||
border: 1px solid ${cssVar.colorSplit};
|
||||
`,
|
||||
}));
|
||||
|
||||
interface ColorChunkProps {
|
||||
value: ColorInput;
|
||||
enablePopover?: boolean;
|
||||
}
|
||||
|
||||
const ColorChunk: React.FC<React.PropsWithChildren<ColorChunkProps>> = (props) => {
|
||||
const { styles, theme } = useStyle();
|
||||
const { value, children, enablePopover } = props;
|
||||
|
||||
const dotColor = React.useMemo(() => new FastColor(value).toHexString(), [value]);
|
||||
|
||||
let dotNode = (
|
||||
<span className={styles.codeSpan}>
|
||||
<span className={styles.dot} style={{ backgroundColor: dotColor }} />
|
||||
{children ?? dotColor}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (enablePopover) {
|
||||
dotNode = (
|
||||
<Popover
|
||||
placement="left"
|
||||
content={<div hidden />}
|
||||
styles={{
|
||||
container: {
|
||||
backgroundColor: dotColor,
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: theme.borderRadiusLG,
|
||||
},
|
||||
root: {
|
||||
'--antd-arrow-background-color': dotColor,
|
||||
backgroundColor: 'transparent',
|
||||
} as React.CSSProperties,
|
||||
}}
|
||||
>
|
||||
{dotNode}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
return dotNode;
|
||||
};
|
||||
|
||||
export default ColorChunk;
|
||||
@@ -1,3 +0,0 @@
|
||||
import ColorPaletteTool from '../../common/Color/ColorPaletteTool';
|
||||
|
||||
export default ColorPaletteTool;
|
||||
@@ -1,3 +0,0 @@
|
||||
import ColorPaletteToolDark from '../../common/Color/ColorPaletteToolDark';
|
||||
|
||||
export default ColorPaletteToolDark;
|
||||
@@ -1,3 +0,0 @@
|
||||
import ColorPalettes from '../../common/Color/ColorPalettes';
|
||||
|
||||
export default ColorPalettes;
|
||||
@@ -1,236 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
BugOutlined,
|
||||
CompassOutlined,
|
||||
EditOutlined,
|
||||
GithubOutlined,
|
||||
HistoryOutlined,
|
||||
IssuesCloseOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { GetProp } from 'antd';
|
||||
import { Descriptions, Flex, theme, Tooltip, Typography } from 'antd';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import copy from 'antd/es/_util/copy';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
|
||||
import useIssueCount from '../../../hooks/useIssueCount';
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import ComponentChangelog from '../../common/ComponentChangelog';
|
||||
import Link from '../../common/Link';
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
import: '使用',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
source: '反馈',
|
||||
docs: '文档',
|
||||
edit: '编辑此页',
|
||||
changelog: '更新日志',
|
||||
design: '设计指南',
|
||||
version: '版本',
|
||||
issueNew: '提交问题',
|
||||
issueOpen: '待解决',
|
||||
},
|
||||
en: {
|
||||
import: 'Import',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
source: 'GitHub',
|
||||
docs: 'Docs',
|
||||
edit: 'Edit this page',
|
||||
changelog: 'Changelog',
|
||||
design: 'Design',
|
||||
version: 'Version',
|
||||
issueNew: 'Issue',
|
||||
issueOpen: 'Open issues',
|
||||
},
|
||||
};
|
||||
|
||||
const branchUrl = (repo: string) => `https://github.com/${repo}/edit/master/`;
|
||||
|
||||
function isVersionNumber(value?: string) {
|
||||
return value && /^\d+\.\d+\.\d+$/.test(value);
|
||||
}
|
||||
|
||||
const transformComponentName = (componentName: string) => {
|
||||
if (componentName === 'Notification' || componentName === 'Message') {
|
||||
return componentName.toLowerCase();
|
||||
}
|
||||
return componentName;
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ cssVar, token }) => ({
|
||||
code: css`
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
column-gap: ${cssVar.paddingXXS};
|
||||
border-radius: ${cssVar.borderRadiusSM};
|
||||
padding-inline: ${cssVar.paddingXXS} !important;
|
||||
transition: all ${cssVar.motionDurationSlow} !important;
|
||||
font-family: ${token.codeFamily};
|
||||
color: ${cssVar.colorTextSecondary} !important;
|
||||
&:hover {
|
||||
background: ${cssVar.controlItemBgHover};
|
||||
}
|
||||
a&:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
`,
|
||||
icon: css`
|
||||
margin-inline-end: 4px;
|
||||
`,
|
||||
}));
|
||||
|
||||
export interface ComponentMetaProps {
|
||||
component: string;
|
||||
source: string | true;
|
||||
filename?: string;
|
||||
version?: string;
|
||||
designUrl?: string;
|
||||
searchTitleKeywords?: string[];
|
||||
repo: string;
|
||||
}
|
||||
|
||||
const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
|
||||
const { component, source, filename, version, designUrl, searchTitleKeywords, repo } = props;
|
||||
const { token } = theme.useToken();
|
||||
const [locale, lang] = useLocale(locales);
|
||||
const isZhCN = lang === 'cn';
|
||||
const { styles } = useStyle();
|
||||
|
||||
// ======================= Issues Count =======================
|
||||
const { issueCount, issueCountLoading, issueNewUrl, issueSearchUrl } = useIssueCount({
|
||||
repo,
|
||||
titleKeywords: searchTitleKeywords,
|
||||
});
|
||||
|
||||
// ========================= Copy =========================
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
const importCode =
|
||||
component === 'Icon'
|
||||
? `import { AntDesignOutlined } from '@ant-design/icons';`
|
||||
: `import { ${transformComponentName(component)} } from 'antd';`;
|
||||
|
||||
const onCopy = async () => {
|
||||
await copy(importCode);
|
||||
setCopied(true);
|
||||
};
|
||||
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
setCopied(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ======================== Source ========================
|
||||
const [filledSource, abbrSource] = React.useMemo(() => {
|
||||
if (String(source) === 'true') {
|
||||
const kebabComponent = kebabCase(component);
|
||||
return [
|
||||
`https://github.com/${repo}/blob/master/components/${kebabComponent}`,
|
||||
`components/${kebabComponent}`,
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof source !== 'string') {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [source, source];
|
||||
}, [component, repo, source]);
|
||||
|
||||
return (
|
||||
<Descriptions
|
||||
size="small"
|
||||
colon={false}
|
||||
column={1}
|
||||
style={{ marginTop: token.margin }}
|
||||
styles={{ label: { paddingInlineEnd: token.padding, width: 56 } }}
|
||||
items={
|
||||
[
|
||||
{
|
||||
label: locale.import,
|
||||
children: (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={copied ? locale.copied : locale.copy}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Typography.Text
|
||||
className={styles.code}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{importCode}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
filledSource && {
|
||||
label: locale.source,
|
||||
children: (
|
||||
<Flex justify="flex-start" align="center" gap="small">
|
||||
<Typography.Link className={styles.code} href={filledSource} target="_blank">
|
||||
<GithubOutlined className={styles.icon} />
|
||||
<span>{abbrSource}</span>
|
||||
</Typography.Link>
|
||||
<Typography.Link className={styles.code} href={issueNewUrl} target="_blank">
|
||||
<BugOutlined className={styles.icon} />
|
||||
<span>{locale.issueNew}</span>
|
||||
</Typography.Link>
|
||||
<Typography.Link className={styles.code} href={issueSearchUrl} target="_blank">
|
||||
<IssuesCloseOutlined className={styles.icon} />
|
||||
<span>
|
||||
{locale.issueOpen} {issueCountLoading ? <LoadingOutlined /> : issueCount}
|
||||
</span>
|
||||
</Typography.Link>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
filename && {
|
||||
label: locale.docs,
|
||||
children: (
|
||||
<Flex justify="flex-start" align="center" gap="small">
|
||||
<Typography.Link
|
||||
className={styles.code}
|
||||
href={`${branchUrl(repo)}${filename}`}
|
||||
target="_blank"
|
||||
>
|
||||
<EditOutlined className={styles.icon} />
|
||||
<span>{locale.edit}</span>
|
||||
</Typography.Link>
|
||||
{designUrl && (
|
||||
<Link className={styles.code} to={designUrl}>
|
||||
<CompassOutlined className={styles.icon} />
|
||||
<span>{locale.design}</span>
|
||||
</Link>
|
||||
)}
|
||||
<ComponentChangelog>
|
||||
<Typography.Link className={styles.code}>
|
||||
<HistoryOutlined className={styles.icon} />
|
||||
<span>{locale.changelog}</span>
|
||||
</Typography.Link>
|
||||
</ComponentChangelog>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
isVersionNumber(version) && {
|
||||
label: locale.version,
|
||||
children: (
|
||||
<Typography.Text className={styles.code}>
|
||||
{isZhCN ? `自 ${version} 起支持` : `supported since ${version}`}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
].filter(Boolean) as GetProp<typeof Descriptions, 'items'>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentMeta;
|
||||
@@ -1,55 +0,0 @@
|
||||
export type Component = {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
cover: string;
|
||||
coverDark?: 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;
|
||||
@@ -1,260 +0,0 @@
|
||||
import React, { memo, useMemo, useRef, useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Affix, Card, Col, Divider, Flex, Input, Row, Tag, Typography } from 'antd';
|
||||
import { createStaticStyles, useTheme } from 'antd-style';
|
||||
import { useIntl, useLocation, useSidebarData } from 'dumi';
|
||||
import debounce from 'lodash/debounce';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
|
||||
import Link from '../../common/Link';
|
||||
import SiteContext from '../../slots/SiteContext';
|
||||
import type { Component } from './ProComponentsList';
|
||||
import proComponentsList from './ProComponentsList';
|
||||
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
componentsOverviewGroupTitle: css`
|
||||
margin-bottom: ${cssVar.marginLG} !important;
|
||||
`,
|
||||
componentsOverviewTitle: css`
|
||||
overflow: hidden;
|
||||
color: ${cssVar.colorTextHeading};
|
||||
text-overflow: ellipsis;
|
||||
`,
|
||||
componentsOverviewImg: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 152px;
|
||||
`,
|
||||
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;
|
||||
}
|
||||
`,
|
||||
componentsOverviewAffix: css`
|
||||
display: flex;
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
justify-content: space-between;
|
||||
`,
|
||||
componentsOverviewSearch: css`
|
||||
padding: 0;
|
||||
box-shadow: none !important;
|
||||
.anticon-search {
|
||||
color: ${cssVar.colorTextDisabled};
|
||||
}
|
||||
`,
|
||||
componentsOverviewContent: css`
|
||||
&:empty:after {
|
||||
display: block;
|
||||
padding: ${cssVar.padding} 0 calc(${cssVar.paddingMD} * 2);
|
||||
color: ${cssVar.colorTextDisabled};
|
||||
text-align: center;
|
||||
border-bottom: 1px solid ${cssVar.colorSplit};
|
||||
content: 'Not Found';
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
const onClickCard = (pathname: string) => {
|
||||
window.gtag?.('event', '点击', {
|
||||
event_category: '组件总览卡片',
|
||||
event_label: pathname,
|
||||
});
|
||||
};
|
||||
|
||||
const reportSearch = debounce<(value: string) => void>((value) => {
|
||||
window.gtag?.('event', '搜索', {
|
||||
event_category: '组件总览卡片',
|
||||
event_label: value,
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const Overview: React.FC = () => {
|
||||
const { isDark } = React.use(SiteContext);
|
||||
|
||||
const data = useSidebarData();
|
||||
const [searchBarAffixed, setSearchBarAffixed] = useState<boolean>(false);
|
||||
|
||||
const token = useTheme();
|
||||
const { borderRadius, colorBgContainer, fontSizeXL, anchorTop } = token;
|
||||
|
||||
const affixedStyle: CSSProperties = {
|
||||
boxShadow: 'rgba(50, 50, 93, 0.25) 0 6px 12px -2px, rgba(0, 0, 0, 0.3) 0 3px 7px -3px',
|
||||
padding: 8,
|
||||
margin: -8,
|
||||
borderRadius,
|
||||
backgroundColor: colorBgContainer,
|
||||
};
|
||||
|
||||
const { search: urlSearch } = useLocation();
|
||||
const { locale, formatMessage } = useIntl();
|
||||
|
||||
const [search, setSearch] = useState<string>(() => {
|
||||
const params = new URLSearchParams(urlSearch);
|
||||
if (params.has('s')) {
|
||||
return params.get('s') || '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
|
||||
if (event.keyCode === 13 && search.trim().length) {
|
||||
sectionRef.current?.querySelector<HTMLElement>(`.${styles.componentsOverviewCard}`)?.click();
|
||||
}
|
||||
};
|
||||
|
||||
const groups = useMemo<{ title: string; children: Component[] }[]>(
|
||||
() =>
|
||||
data
|
||||
.filter((item) => item?.title)
|
||||
.map<{ title: string; children: Component[] }>((item) => ({
|
||||
title: item?.title || '',
|
||||
children: item.children.map((child) => ({
|
||||
title: child.frontmatter?.title || '',
|
||||
subtitle: child.frontmatter?.subtitle,
|
||||
cover: child.frontmatter?.cover,
|
||||
coverDark: child.frontmatter?.coverDark,
|
||||
link: child.link,
|
||||
})),
|
||||
}))
|
||||
.concat([
|
||||
{
|
||||
title: locale === 'zh-CN' ? '重型组件' : 'Others',
|
||||
children:
|
||||
locale === 'zh-CN'
|
||||
? proComponentsList
|
||||
: proComponentsList.map((component) => ({ ...component, subtitle: '' })),
|
||||
},
|
||||
]),
|
||||
[data, locale],
|
||||
);
|
||||
return (
|
||||
<section className="markdown" ref={sectionRef}>
|
||||
<Divider />
|
||||
<Affix offsetTop={anchorTop} onChange={(affixed) => setSearchBarAffixed(!!affixed)}>
|
||||
<div
|
||||
className={styles.componentsOverviewAffix}
|
||||
style={searchBarAffixed ? affixedStyle : {}}
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
value={search}
|
||||
placeholder={formatMessage({ id: 'app.components.overview.search' })}
|
||||
className={styles.componentsOverviewSearch}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
reportSearch(e.target.value);
|
||||
if (sectionRef.current && searchBarAffixed) {
|
||||
scrollIntoView(sectionRef.current, {
|
||||
scrollMode: 'if-needed',
|
||||
block: 'start',
|
||||
behavior: (actions) =>
|
||||
actions.forEach(({ el, top }) => {
|
||||
el.scrollTop = top - 64;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
variant="borderless"
|
||||
suffix={<SearchOutlined />}
|
||||
style={{ fontSize: searchBarAffixed ? fontSizeXL - 2 : fontSizeXL }}
|
||||
/>
|
||||
</div>
|
||||
</Affix>
|
||||
<Divider />
|
||||
<div className={styles.componentsOverviewContent}>
|
||||
{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}>
|
||||
<Title level={2} className={styles.componentsOverviewGroupTitle}>
|
||||
<Flex gap="small" align="center">
|
||||
<span style={{ fontSize: 24 }}>{group?.title}</span>
|
||||
<Tag style={{ display: 'block' }}>{components.length}</Tag>
|
||||
</Flex>
|
||||
</Title>
|
||||
<Row gutter={[24, 24]}>
|
||||
{components.map((component) => {
|
||||
let url = component.link;
|
||||
/** 是否是外链 */
|
||||
const isExternalLink = url.startsWith('http');
|
||||
|
||||
if (!isExternalLink) {
|
||||
url += urlSearch;
|
||||
}
|
||||
|
||||
const cardContent = (
|
||||
<Card
|
||||
key={component.title}
|
||||
onClick={() => onClickCard(url)}
|
||||
styles={{
|
||||
body: {
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'bottom right',
|
||||
backgroundImage: `url(${component.tag || ''})`,
|
||||
},
|
||||
}}
|
||||
size="small"
|
||||
className={styles.componentsOverviewCard}
|
||||
title={
|
||||
<div className={styles.componentsOverviewTitle}>
|
||||
{component.title} {component.subtitle}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.componentsOverviewImg}>
|
||||
<img
|
||||
draggable={false}
|
||||
src={
|
||||
isDark && component.coverDark ? component.coverDark : component.cover
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const linkContent = isExternalLink ? (
|
||||
<a href={url} key={component.title}>
|
||||
{cardContent}
|
||||
</a>
|
||||
) : (
|
||||
<Link to={url} key={component.title}>
|
||||
{cardContent}
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<Col xs={24} sm={12} lg={8} xl={6} key={component.title}>
|
||||
{linkContent}
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Overview);
|
||||
@@ -1,251 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { LinkOutlined, QuestionCircleOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { ConfigProvider, Flex, Popover, Table, Typography } from 'antd';
|
||||
import { createStyles, css, useTheme } from 'antd-style';
|
||||
import { getDesignToken } from 'antd-token-previewer';
|
||||
import tokenMeta from 'antd/es/version/token-meta.json';
|
||||
import tokenData from 'antd/es/version/token.json';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import { useColumns } from '../TokenTable';
|
||||
import type { TokenData } from '../TokenTable';
|
||||
|
||||
const compare = (token1: string, token2: string) => {
|
||||
const hasColor1 = token1.toLowerCase().includes('color');
|
||||
const hasColor2 = token2.toLowerCase().includes('color');
|
||||
if (hasColor1 && !hasColor2) {
|
||||
return -1;
|
||||
}
|
||||
if (!hasColor1 && hasColor2) {
|
||||
return 1;
|
||||
}
|
||||
return token1 < token2 ? -1 : 1;
|
||||
};
|
||||
|
||||
const defaultToken = getDesignToken();
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
token: 'Token 名称',
|
||||
description: '描述',
|
||||
type: '类型',
|
||||
value: '默认值',
|
||||
componentToken: '组件 Token',
|
||||
globalToken: '全局 Token',
|
||||
componentComment: '这里是你的组件 token',
|
||||
globalComment: '这里是你的全局 token',
|
||||
help: '如何定制?',
|
||||
customizeTokenLink: '/docs/react/customize-theme-cn#修改主题变量',
|
||||
customizeComponentTokenLink: '/docs/react/customize-theme-cn#修改组件变量',
|
||||
},
|
||||
en: {
|
||||
token: 'Token Name',
|
||||
description: 'Description',
|
||||
type: 'Type',
|
||||
value: 'Default Value',
|
||||
componentToken: 'Component Token',
|
||||
globalToken: 'Global Token',
|
||||
componentComment: 'here is your component tokens',
|
||||
globalComment: 'here is your global tokens',
|
||||
help: 'How to use?',
|
||||
customizeTokenLink: '/docs/react/customize-theme#customize-design-token',
|
||||
customizeComponentTokenLink: 'docs/react/customize-theme#customize-component-token',
|
||||
},
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ cssVar }) => ({
|
||||
tableTitle: css`
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
line-height: 40px;
|
||||
gap: ${cssVar.marginXS};
|
||||
`,
|
||||
arrowIcon: css`
|
||||
font-size: ${cssVar.fontSizeLG};
|
||||
& svg {
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
}
|
||||
`,
|
||||
help: css`
|
||||
font-size: ${cssVar.fontSizeSM};
|
||||
font-weight: normal;
|
||||
color: #999;
|
||||
a {
|
||||
color: #999;
|
||||
}
|
||||
`,
|
||||
tokenTitle: css`
|
||||
font-size: ${cssVar.fontSizeLG};
|
||||
font-weight: bold;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface SubTokenTableProps {
|
||||
defaultOpen?: boolean;
|
||||
title: string;
|
||||
helpText: React.ReactNode;
|
||||
helpLink: string;
|
||||
tokens: string[];
|
||||
component?: string;
|
||||
comment?: {
|
||||
componentComment?: string;
|
||||
globalComment?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const SubTokenTable: React.FC<SubTokenTableProps> = (props) => {
|
||||
const { defaultOpen, tokens, title, helpText, helpLink, component, comment } = props;
|
||||
const [, lang] = useLocale(locales);
|
||||
const token = useTheme();
|
||||
const columns = useColumns();
|
||||
|
||||
const [open, setOpen] = useState<boolean>(defaultOpen ?? process.env.NODE_ENV !== 'production');
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
if (!tokens.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = tokens
|
||||
.sort(component ? undefined : compare)
|
||||
.map<TokenData>((name) => {
|
||||
const meta = component
|
||||
? tokenMeta.components[component].find((item) => item.token === name)
|
||||
: tokenMeta.global[name];
|
||||
|
||||
if (!meta) {
|
||||
return null as unknown as TokenData;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
desc: lang === 'cn' ? meta.desc : meta.descEn,
|
||||
type: meta.type,
|
||||
value: component ? tokenData[component].component[name] : defaultToken[name],
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const code = component
|
||||
? `<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
${component}: {
|
||||
/* ${comment?.componentComment} */
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
...
|
||||
</ConfigProvider>`
|
||||
: `<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
/* ${comment?.globalComment} */
|
||||
},
|
||||
}}
|
||||
>
|
||||
...
|
||||
</ConfigProvider>`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.tableTitle} onClick={() => setOpen(!open)}>
|
||||
<RightOutlined className={styles.arrowIcon} rotate={open ? 90 : 0} />
|
||||
<Flex className={styles.tokenTitle} gap="small" justify="flex-start" align="center">
|
||||
{title}
|
||||
<Popover
|
||||
title={null}
|
||||
destroyOnHidden
|
||||
styles={{ root: { width: 400 } }}
|
||||
content={
|
||||
<Typography>
|
||||
<pre dir="ltr" style={{ fontSize: 12 }}>
|
||||
<code dir="ltr">{code}</code>
|
||||
</pre>
|
||||
<a href={helpLink} target="_blank" rel="noreferrer">
|
||||
<LinkOutlined style={{ marginInlineEnd: 4 }} />
|
||||
{helpText}
|
||||
</a>
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<span className={styles.help}>
|
||||
<QuestionCircleOutlined style={{ marginInlineEnd: 4 }} />
|
||||
{helpText}
|
||||
</span>
|
||||
</Popover>
|
||||
</Flex>
|
||||
</div>
|
||||
{open && (
|
||||
<ConfigProvider theme={{ token: { borderRadius: 0 } }}>
|
||||
<Table<TokenData>
|
||||
size="middle"
|
||||
columns={columns}
|
||||
bordered
|
||||
dataSource={data}
|
||||
style={{ marginBottom: token.margin }}
|
||||
pagination={false}
|
||||
rowKey={(record) => record.name}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ComponentTokenTableProps {
|
||||
component: string;
|
||||
}
|
||||
|
||||
const ComponentTokenTable: React.FC<ComponentTokenTableProps> = ({ component }) => {
|
||||
const [locale] = useLocale(locales);
|
||||
const [mergedGlobalTokens] = useMemo(() => {
|
||||
const globalTokenSet = new Set<string>();
|
||||
|
||||
component.split(',').forEach((comp) => {
|
||||
const { global: globalTokens = [] } = tokenData[comp] || {};
|
||||
|
||||
globalTokens.forEach((token: string) => {
|
||||
globalTokenSet.add(token);
|
||||
});
|
||||
});
|
||||
|
||||
return [Array.from(globalTokenSet)] as const;
|
||||
}, [component]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tokenMeta.components[component] && (
|
||||
<SubTokenTable
|
||||
defaultOpen
|
||||
title={locale.componentToken}
|
||||
helpText={locale.help}
|
||||
helpLink={locale.customizeTokenLink}
|
||||
tokens={tokenMeta.components[component].map((item) => item.token)}
|
||||
component={component}
|
||||
comment={{
|
||||
componentComment: locale.componentComment,
|
||||
globalComment: locale.globalComment,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SubTokenTable
|
||||
title={locale.globalToken}
|
||||
helpText={locale.help}
|
||||
helpLink={locale.customizeComponentTokenLink}
|
||||
tokens={mergedGlobalTokens}
|
||||
comment={{
|
||||
componentComment: locale.componentComment,
|
||||
globalComment: locale.globalComment,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ComponentTokenTable);
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* copied: https://github.com/arvinxx/dumi-theme-antd-style/tree/master/src/builtins/Container
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { Alert } from 'antd';
|
||||
|
||||
import useStyles from './style';
|
||||
|
||||
interface ContainerProps {
|
||||
type: 'info' | 'warning' | 'success' | 'error';
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const Container: React.FC<React.PropsWithChildren<ContainerProps>> = ({
|
||||
type,
|
||||
title,
|
||||
children,
|
||||
}) => {
|
||||
const { styles, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<Alert
|
||||
showIcon
|
||||
type={type}
|
||||
title={title || type.toUpperCase()}
|
||||
description={
|
||||
<div
|
||||
className={cx(
|
||||
styles.desc,
|
||||
// 为了让 markdown 的样式生效,需要在这里添加一个额外的 class
|
||||
'markdown',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
className={styles.alert}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Container;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyles = createStyles(({ prefixCls, css }) => ({
|
||||
alert: css`
|
||||
padding: 12px 16px;
|
||||
.${prefixCls}-alert-message {
|
||||
font-weight: bold;
|
||||
}
|
||||
`,
|
||||
|
||||
/* 使用 `&&` 加一点点权重 */
|
||||
desc: css`
|
||||
&& p {
|
||||
margin: 0;
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
export default useStyles;
|
||||
@@ -1,99 +0,0 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { BugOutlined, CodeOutlined } from '@ant-design/icons';
|
||||
import { css, Global } from '@emotion/react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { DumiDemo, DumiDemoGrid, FormattedMessage } from 'dumi';
|
||||
|
||||
import useLayoutState from '../../../hooks/useLayoutState';
|
||||
import DemoContext from '../../slots/DemoContext';
|
||||
import DemoFallback from '../Previewer/DemoFallback';
|
||||
|
||||
const DemoWrapper: typeof DumiDemoGrid = ({ items }) => {
|
||||
const { showDebug, setShowDebug } = React.use(DemoContext);
|
||||
|
||||
const [expandAll, setExpandAll] = useLayoutState(false);
|
||||
|
||||
const handleVisibleToggle = () => {
|
||||
setShowDebug?.(!showDebug);
|
||||
};
|
||||
|
||||
const handleExpandToggle = () => {
|
||||
setExpandAll(!expandAll);
|
||||
};
|
||||
|
||||
const demos = React.useMemo(
|
||||
() =>
|
||||
items.reduce<typeof items>((acc, item) => {
|
||||
const { previewerProps } = item;
|
||||
const { debug } = previewerProps;
|
||||
if (debug && !showDebug) {
|
||||
return acc;
|
||||
}
|
||||
return acc.concat({
|
||||
...item,
|
||||
previewerProps: {
|
||||
...previewerProps,
|
||||
expand: expandAll,
|
||||
// always override debug property, because dumi will hide debug demo in production
|
||||
debug: false,
|
||||
/**
|
||||
* antd extra marker for the original debug
|
||||
* @see https://github.com/ant-design/ant-design/pull/40130#issuecomment-1380208762
|
||||
*/
|
||||
originDebug: debug,
|
||||
},
|
||||
});
|
||||
}, []),
|
||||
[expandAll, items, showDebug],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="demo-wrapper">
|
||||
<Global
|
||||
styles={css`
|
||||
:root {
|
||||
--antd-site-api-deprecated-display: ${showDebug ? 'table-row' : 'none'};
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<span className="all-code-box-controls">
|
||||
<Tooltip
|
||||
title={
|
||||
<FormattedMessage id={`app.component.examples.${expandAll ? 'collapse' : 'expand'}`} />
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CodeOutlined />}
|
||||
onClick={handleExpandToggle}
|
||||
className={expandAll ? 'icon-enabled' : ''}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
<FormattedMessage id={`app.component.examples.${showDebug ? 'hide' : 'visible'}`} />
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<BugOutlined />}
|
||||
onClick={handleVisibleToggle}
|
||||
className={showDebug ? 'icon-enabled' : ''}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<DumiDemoGrid
|
||||
items={demos}
|
||||
demoRender={(item) => (
|
||||
<Suspense key={item.demo.id} fallback={<DemoFallback />}>
|
||||
<DumiDemo {...item} />
|
||||
</Suspense>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoWrapper;
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Flex } from 'antd';
|
||||
import type { FlexProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import ImagePreview from '../ImagePreview';
|
||||
import type { ImagePreviewProps } from '../ImagePreview';
|
||||
|
||||
const isNonNullable = <T,>(val: T): val is NonNullable<T> => {
|
||||
return val !== undefined && val !== null;
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ css, token }) => {
|
||||
return {
|
||||
wrapper: css`
|
||||
color: ${token.colorText};
|
||||
font-size: ${token.fontSize}px;
|
||||
line-height: 2;
|
||||
`,
|
||||
title: css`
|
||||
margin: 1em 0;
|
||||
`,
|
||||
description: css`
|
||||
margin: 1em 0;
|
||||
padding-inline-start: 0.8em;
|
||||
color: ${token.colorTextSecondary};
|
||||
font-size: 90%;
|
||||
border-inline-start: 4px solid ${token.colorSplit};
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface FlexWithImagePreviewProps {
|
||||
imagePreviewProps?: ImagePreviewProps;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const FlexWithImagePreview: React.FC<
|
||||
FlexWithImagePreviewProps & React.PropsWithChildren<FlexProps>
|
||||
> = (props) => {
|
||||
const { imagePreviewProps, title, description, className, style, children, ...rest } = props;
|
||||
const { styles } = useStyle();
|
||||
if (!title && !description) {
|
||||
return <ImagePreview {...imagePreviewProps}>{children}</ImagePreview>;
|
||||
}
|
||||
return (
|
||||
<Flex className={clsx(styles.wrapper, className)} style={style} {...rest}>
|
||||
<Flex align="flex-start" justify="flex-start" vertical>
|
||||
{isNonNullable(title) && <div className={styles.title}>{title}</div>}
|
||||
{isNonNullable(description) && <div className={styles.description}>{description}</div>}
|
||||
</Flex>
|
||||
<ImagePreview {...imagePreviewProps}>{children}</ImagePreview>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlexWithImagePreview;
|
||||
@@ -1,83 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { App } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { useIntl } from 'dumi';
|
||||
|
||||
import CopyableIcon from './CopyableIcon';
|
||||
import type { CategoriesKeys } from './fields';
|
||||
import type { ThemeType } from './IconSearch';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
anticonsList: css`
|
||||
margin: ${cssVar.margin} 0;
|
||||
overflow: hidden;
|
||||
direction: ltr;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-gap: ${cssVar.margin};
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
padding: 0;
|
||||
`,
|
||||
copiedCode: css`
|
||||
padding: 0 ${cssVar.paddingXXS};
|
||||
font-size: ${cssVar.fontSizeSM};
|
||||
background-color: ${cssVar.colorBgLayout};
|
||||
border-radius: ${cssVar.borderRadiusXS};
|
||||
`,
|
||||
}));
|
||||
|
||||
interface CategoryProps {
|
||||
title: CategoriesKeys;
|
||||
icons: string[];
|
||||
theme: ThemeType;
|
||||
newIcons: ReadonlyArray<string> | string[];
|
||||
}
|
||||
|
||||
const Category: React.FC<CategoryProps> = (props) => {
|
||||
const { message } = App.useApp();
|
||||
const { icons, title, newIcons, theme } = props;
|
||||
const intl = useIntl();
|
||||
const [justCopied, setJustCopied] = React.useState<string | null>(null);
|
||||
const copyId = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const onCopied = React.useCallback(
|
||||
(type: string, text: string) => {
|
||||
message.success(
|
||||
<span>
|
||||
<code className={styles.copiedCode}>{text}</code> copied 🎉
|
||||
</span>,
|
||||
);
|
||||
setJustCopied(type);
|
||||
copyId.current = setTimeout(() => {
|
||||
setJustCopied(null);
|
||||
}, 2000);
|
||||
},
|
||||
[message, styles.copiedCode],
|
||||
);
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
if (copyId.current) {
|
||||
clearTimeout(copyId.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<h3>{intl.formatMessage({ id: `app.docs.components.icon.category.${title}` })}</h3>
|
||||
<ul className={styles.anticonsList}>
|
||||
{icons.map((name) => (
|
||||
<CopyableIcon
|
||||
key={name}
|
||||
name={name}
|
||||
theme={theme}
|
||||
isNew={newIcons.includes(name)}
|
||||
justCopied={justCopied}
|
||||
onCopied={onCopied}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Category;
|
||||
@@ -1,134 +0,0 @@
|
||||
import React from 'react';
|
||||
import * as AntdIcons from '@ant-design/icons';
|
||||
import { App, Badge } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import copy from 'antd/es/_util/copy';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import type { ThemeType } from './IconSearch';
|
||||
|
||||
const allIcons: { [key: PropertyKey]: any } = AntdIcons;
|
||||
|
||||
const useStyle = createStyles(({ cssVar, token, css }) => {
|
||||
const { antCls, iconCls } = token;
|
||||
return {
|
||||
iconItem: css`
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-inline-start: 0 !important;
|
||||
margin-inline-end: 0 !important;
|
||||
padding-inline-start: 0 !important;
|
||||
padding-inline-end: 0 !important;
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
background-color: inherit;
|
||||
border-radius: ${cssVar.borderRadiusSM};
|
||||
cursor: pointer;
|
||||
transition: all ${cssVar.motionDurationSlow} ease-in-out;
|
||||
${token.iconCls} {
|
||||
margin: ${cssVar.marginXS} 0;
|
||||
font-size: 36px;
|
||||
transition: transform ${cssVar.motionDurationSlow} ease-in-out;
|
||||
will-change: transform;
|
||||
}
|
||||
&:hover {
|
||||
color: ${cssVar.colorWhite};
|
||||
background-color: ${cssVar.colorPrimary};
|
||||
${iconCls} {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
${antCls}-badge {
|
||||
color: ${cssVar.colorWhite};
|
||||
}
|
||||
}
|
||||
&.TwoTone:hover {
|
||||
background-color: #8ecafe;
|
||||
}
|
||||
&.copied:hover {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
&::after {
|
||||
content: 'Copied!';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: 100px;
|
||||
color: ${cssVar.colorTextLightSolid};
|
||||
text-align: center;
|
||||
background-color: ${cssVar.colorPrimary};
|
||||
opacity: 0;
|
||||
transition: all ${cssVar.motionDurationSlow} cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
||||
}
|
||||
&.copied::after {
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
anticonCls: css`
|
||||
display: block;
|
||||
font-family: 'Lucida Console', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
transform: scale(0.8);
|
||||
${antCls}-badge {
|
||||
transition: color ${cssVar.motionDurationSlow} ease-in-out;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
errMessage: '复制名称失败,请重试',
|
||||
},
|
||||
en: {
|
||||
errMessage: 'Copy icon name failed, please try again.',
|
||||
},
|
||||
};
|
||||
|
||||
export interface CopyableIconProps {
|
||||
name: string;
|
||||
isNew: boolean;
|
||||
theme: ThemeType;
|
||||
justCopied: string | null;
|
||||
onCopied: (type: string, text: string) => void;
|
||||
}
|
||||
|
||||
const CopyableIcon: React.FC<CopyableIconProps> = (props) => {
|
||||
const { message } = App.useApp();
|
||||
const { name, isNew, justCopied, theme, onCopied } = props;
|
||||
const [locale] = useLocale(locales);
|
||||
const { styles } = useStyle();
|
||||
|
||||
const onCopy = async (text: string) => {
|
||||
const result = await copy(text);
|
||||
if (result) {
|
||||
onCopied(name, text);
|
||||
} else {
|
||||
message.error(locale.errMessage);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<li
|
||||
className={clsx(theme, styles.iconItem, { copied: justCopied === name })}
|
||||
onClick={() => onCopy(`<${name} />`)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{React.createElement(allIcons[name])}
|
||||
<span className={styles.anticonCls}>
|
||||
<Badge dot={isNew}>{name}</Badge>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyableIcon;
|
||||
@@ -1,227 +0,0 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import Icon, * as AntdIcons from '@ant-design/icons';
|
||||
import { Affix, Empty, Input, Segmented } from 'antd';
|
||||
import { createStaticStyles, useTheme } from 'antd-style';
|
||||
import type { SegmentedOptions } from 'antd/es/segmented';
|
||||
import { useIntl } from 'dumi';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import Category from './Category';
|
||||
import type { CategoriesKeys } from './fields';
|
||||
import { all, categories } from './fields';
|
||||
import metaInfo from './meta';
|
||||
import type { IconName, IconsMeta } from './meta';
|
||||
import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons';
|
||||
|
||||
export enum ThemeType {
|
||||
Filled = 'Filled',
|
||||
Outlined = 'Outlined',
|
||||
TwoTone = 'TwoTone',
|
||||
}
|
||||
|
||||
const allIcons: { [key: string]: any } = AntdIcons;
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
iconSearchAffix: css`
|
||||
display: flex;
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
justify-content: space-between;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface IconSearchState {
|
||||
theme: ThemeType;
|
||||
searchKey: string;
|
||||
}
|
||||
|
||||
const NEW_ICON_NAMES: ReadonlyArray<string> = [];
|
||||
|
||||
const IconSearch: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [displayState, setDisplayState] = useState<IconSearchState>({
|
||||
searchKey: '',
|
||||
theme: ThemeType.Outlined,
|
||||
});
|
||||
const token = useTheme();
|
||||
|
||||
const handleSearchIcon = debounce((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDisplayState((prevState) => ({ ...prevState, searchKey: e.target.value }));
|
||||
|
||||
document.getElementById('list-of-icons')?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 300);
|
||||
|
||||
const handleChangeTheme = useCallback((value: ThemeType) => {
|
||||
setDisplayState((prevState) => ({ ...prevState, theme: value as ThemeType }));
|
||||
}, []);
|
||||
|
||||
const renderCategories = useMemo<React.ReactNode | React.ReactNode[]>(() => {
|
||||
const { searchKey = '', theme } = displayState;
|
||||
// loop over metaInfo to find all the icons which has searchKey in their tags
|
||||
let normalizedSearchKey = searchKey?.trim();
|
||||
|
||||
if (normalizedSearchKey) {
|
||||
normalizedSearchKey = normalizedSearchKey
|
||||
.replace(/^<([a-z]*)\s\/>$/gi, (_, name) => name)
|
||||
.replace(/(Filled|Outlined|TwoTone)$/, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
const tagMatchedCategoryObj = matchCategoriesFromTag(normalizedSearchKey, metaInfo);
|
||||
|
||||
const namedMatchedCategoryObj = Object.keys(categories).reduce<Record<string, MatchedCategory>>(
|
||||
(acc, key) => {
|
||||
let iconList = categories[key as CategoriesKeys];
|
||||
if (normalizedSearchKey) {
|
||||
const matchKey = normalizedSearchKey;
|
||||
iconList = iconList.filter((iconName) => iconName.toLowerCase().includes(matchKey));
|
||||
}
|
||||
|
||||
const ignore = [
|
||||
'CopyrightCircle', // same as Copyright
|
||||
'DollarCircle', // same as Dollar
|
||||
];
|
||||
iconList = iconList.filter((icon) => !ignore.includes(icon));
|
||||
|
||||
acc[key] = {
|
||||
category: key,
|
||||
icons: iconList,
|
||||
};
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
// merge matched categories from tag search
|
||||
const merged = mergeCategory(namedMatchedCategoryObj, tagMatchedCategoryObj);
|
||||
const matchedCategories = Object.values(merged)
|
||||
.map((item) => {
|
||||
item.icons = item.icons
|
||||
.map((iconName) => iconName + theme)
|
||||
.filter((iconName) => allIcons[iconName]);
|
||||
|
||||
return item;
|
||||
})
|
||||
.filter(({ icons }) => !!icons.length);
|
||||
|
||||
const categoriesResult = matchedCategories.map(({ category, icons }) => (
|
||||
<Category
|
||||
key={category}
|
||||
title={category as CategoriesKeys}
|
||||
theme={theme}
|
||||
icons={icons}
|
||||
newIcons={NEW_ICON_NAMES}
|
||||
/>
|
||||
));
|
||||
return categoriesResult.length ? categoriesResult : <Empty style={{ margin: '2em 0' }} />;
|
||||
}, [displayState]);
|
||||
|
||||
const [searchBarAffixed, setSearchBarAffixed] = useState<boolean | undefined>(false);
|
||||
|
||||
const { borderRadius, colorBgContainer, anchorTop } = token;
|
||||
|
||||
const affixedStyle: CSSProperties = {
|
||||
boxShadow: 'rgba(50, 50, 93, 0.25) 0 6px 12px -2px, rgba(0, 0, 0, 0.3) 0 3px 7px -3px',
|
||||
padding: 8,
|
||||
margin: -8,
|
||||
borderRadius,
|
||||
backgroundColor: colorBgContainer,
|
||||
};
|
||||
|
||||
const memoizedOptions = React.useMemo<SegmentedOptions<ThemeType>>(
|
||||
() => [
|
||||
{
|
||||
value: ThemeType.Outlined,
|
||||
icon: <Icon component={OutlinedIcon} />,
|
||||
label: intl.formatMessage({ id: 'app.docs.components.icon.outlined' }),
|
||||
},
|
||||
{
|
||||
value: ThemeType.Filled,
|
||||
icon: <Icon component={FilledIcon} />,
|
||||
label: intl.formatMessage({ id: 'app.docs.components.icon.filled' }),
|
||||
},
|
||||
{
|
||||
value: ThemeType.TwoTone,
|
||||
icon: <Icon component={TwoToneIcon} />,
|
||||
label: intl.formatMessage({ id: 'app.docs.components.icon.two-tone' }),
|
||||
},
|
||||
],
|
||||
[intl],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="markdown">
|
||||
<Affix offsetTop={anchorTop} onChange={setSearchBarAffixed}>
|
||||
<div className={styles.iconSearchAffix} style={searchBarAffixed ? affixedStyle : {}}>
|
||||
<Segmented<ThemeType>
|
||||
size="large"
|
||||
value={displayState.theme}
|
||||
options={memoizedOptions}
|
||||
onChange={handleChangeTheme}
|
||||
/>
|
||||
<Input.Search
|
||||
placeholder={intl.formatMessage(
|
||||
{ id: 'app.docs.components.icon.search.placeholder' },
|
||||
{ total: all.length },
|
||||
)}
|
||||
style={{ flex: 1, marginInlineStart: 16 }}
|
||||
allowClear
|
||||
autoFocus
|
||||
size="large"
|
||||
onChange={handleSearchIcon}
|
||||
/>
|
||||
</div>
|
||||
</Affix>
|
||||
{renderCategories}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSearch;
|
||||
|
||||
type MatchedCategory = {
|
||||
category: string;
|
||||
icons: string[];
|
||||
};
|
||||
|
||||
function matchCategoriesFromTag(searchKey: string, metaInfo: IconsMeta) {
|
||||
if (!searchKey) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.keys(metaInfo).reduce<Record<string, MatchedCategory>>((acc, key) => {
|
||||
const icon = metaInfo[key as IconName];
|
||||
const category = icon.category;
|
||||
|
||||
if (icon.tags.some((tag) => tag.toLowerCase().includes(searchKey))) {
|
||||
if (acc[category]) {
|
||||
// if category exists, push icon to icons array
|
||||
acc[category].icons.push(key);
|
||||
} else {
|
||||
// if category does not exist, create a new entry
|
||||
acc[category] = { category, icons: [key] };
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function mergeCategory(
|
||||
categoryA: Record<string, MatchedCategory>,
|
||||
categoryB: Record<string, MatchedCategory>,
|
||||
) {
|
||||
const merged: Record<string, MatchedCategory> = { ...categoryA };
|
||||
|
||||
Object.keys(categoryB).forEach((key) => {
|
||||
if (merged[key]) {
|
||||
// merge icons array and remove duplicates
|
||||
merged[key].icons = Array.from(new Set([...merged[key].icons, ...categoryB[key].icons]));
|
||||
} else {
|
||||
merged[key] = categoryB[key];
|
||||
}
|
||||
});
|
||||
|
||||
return merged;
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
import * as AntdIcons from '@ant-design/icons/lib/icons';
|
||||
|
||||
export const all = Object.keys(AntdIcons)
|
||||
.map((n) => n.replace(/(Outlined|Filled|TwoTone)$/, ''))
|
||||
.filter((n, i, arr) => arr.indexOf(n) === i);
|
||||
|
||||
const direction = [
|
||||
'StepBackward',
|
||||
'StepForward',
|
||||
'FastBackward',
|
||||
'FastForward',
|
||||
'Shrink',
|
||||
'ArrowsAlt',
|
||||
'Down',
|
||||
'Up',
|
||||
'Left',
|
||||
'Right',
|
||||
'CaretUp',
|
||||
'CaretDown',
|
||||
'CaretLeft',
|
||||
'CaretRight',
|
||||
'UpCircle',
|
||||
'DownCircle',
|
||||
'LeftCircle',
|
||||
'RightCircle',
|
||||
'DoubleRight',
|
||||
'DoubleLeft',
|
||||
'VerticalLeft',
|
||||
'VerticalRight',
|
||||
'VerticalAlignTop',
|
||||
'VerticalAlignMiddle',
|
||||
'VerticalAlignBottom',
|
||||
'Forward',
|
||||
'Backward',
|
||||
'Rollback',
|
||||
'Enter',
|
||||
'Retweet',
|
||||
'Swap',
|
||||
'SwapLeft',
|
||||
'SwapRight',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'PlayCircle',
|
||||
'UpSquare',
|
||||
'DownSquare',
|
||||
'LeftSquare',
|
||||
'RightSquare',
|
||||
'Login',
|
||||
'Logout',
|
||||
'MenuFold',
|
||||
'MenuUnfold',
|
||||
'BorderBottom',
|
||||
'BorderHorizontal',
|
||||
'BorderInner',
|
||||
'BorderOuter',
|
||||
'BorderLeft',
|
||||
'BorderRight',
|
||||
'BorderTop',
|
||||
'BorderVerticle',
|
||||
'PicCenter',
|
||||
'PicLeft',
|
||||
'PicRight',
|
||||
'RadiusBottomleft',
|
||||
'RadiusBottomright',
|
||||
'RadiusUpleft',
|
||||
'RadiusUpright',
|
||||
'Fullscreen',
|
||||
'FullscreenExit',
|
||||
];
|
||||
|
||||
const suggestion = [
|
||||
'Question',
|
||||
'QuestionCircle',
|
||||
'Plus',
|
||||
'PlusCircle',
|
||||
'Pause',
|
||||
'PauseCircle',
|
||||
'Minus',
|
||||
'MinusCircle',
|
||||
'PlusSquare',
|
||||
'MinusSquare',
|
||||
'Info',
|
||||
'InfoCircle',
|
||||
'Exclamation',
|
||||
'ExclamationCircle',
|
||||
'Close',
|
||||
'CloseCircle',
|
||||
'CloseSquare',
|
||||
'Check',
|
||||
'CheckCircle',
|
||||
'CheckSquare',
|
||||
'ClockCircle',
|
||||
'Warning',
|
||||
'IssuesClose',
|
||||
'Stop',
|
||||
];
|
||||
|
||||
const editor = [
|
||||
'Edit',
|
||||
'Form',
|
||||
'Copy',
|
||||
'Scissor',
|
||||
'Delete',
|
||||
'Snippets',
|
||||
'Diff',
|
||||
'Highlight',
|
||||
'AlignCenter',
|
||||
'AlignLeft',
|
||||
'AlignRight',
|
||||
'BgColors',
|
||||
'Bold',
|
||||
'Italic',
|
||||
'Underline',
|
||||
'Strikethrough',
|
||||
'Redo',
|
||||
'Undo',
|
||||
'ZoomIn',
|
||||
'ZoomOut',
|
||||
'FontColors',
|
||||
'FontSize',
|
||||
'LineHeight',
|
||||
'Dash',
|
||||
'SmallDash',
|
||||
'SortAscending',
|
||||
'SortDescending',
|
||||
'Drag',
|
||||
'OrderedList',
|
||||
'UnorderedList',
|
||||
'RadiusSetting',
|
||||
'ColumnWidth',
|
||||
'ColumnHeight',
|
||||
];
|
||||
|
||||
const data = [
|
||||
'AreaChart',
|
||||
'PieChart',
|
||||
'BarChart',
|
||||
'DotChart',
|
||||
'LineChart',
|
||||
'RadarChart',
|
||||
'HeatMap',
|
||||
'Fall',
|
||||
'Rise',
|
||||
'Stock',
|
||||
'BoxPlot',
|
||||
'Fund',
|
||||
'Sliders',
|
||||
];
|
||||
|
||||
const logo = [
|
||||
'Android',
|
||||
'Apple',
|
||||
'Windows',
|
||||
'Ie',
|
||||
'Chrome',
|
||||
'Github',
|
||||
'Aliwangwang',
|
||||
'Dingding',
|
||||
'WeiboSquare',
|
||||
'WeiboCircle',
|
||||
'TaobaoCircle',
|
||||
'Html5',
|
||||
'Weibo',
|
||||
'Twitter',
|
||||
'Wechat',
|
||||
'WhatsApp',
|
||||
'Youtube',
|
||||
'AlipayCircle',
|
||||
'Taobao',
|
||||
'Dingtalk',
|
||||
'Skype',
|
||||
'Qq',
|
||||
'MediumWorkmark',
|
||||
'Gitlab',
|
||||
'Medium',
|
||||
'Linkedin',
|
||||
'GooglePlus',
|
||||
'Dropbox',
|
||||
'Facebook',
|
||||
'Codepen',
|
||||
'CodeSandbox',
|
||||
'CodeSandboxCircle',
|
||||
'Amazon',
|
||||
'Google',
|
||||
'CodepenCircle',
|
||||
'Alipay',
|
||||
'AntDesign',
|
||||
'AntCloud',
|
||||
'Aliyun',
|
||||
'Zhihu',
|
||||
'Slack',
|
||||
'SlackSquare',
|
||||
'Behance',
|
||||
'BehanceSquare',
|
||||
'Dribbble',
|
||||
'DribbbleSquare',
|
||||
'Instagram',
|
||||
'Yuque',
|
||||
'Alibaba',
|
||||
'Yahoo',
|
||||
'Reddit',
|
||||
'Sketch',
|
||||
'WechatWork',
|
||||
'OpenAI',
|
||||
'Discord',
|
||||
'X',
|
||||
'Bilibili',
|
||||
'Pinterest',
|
||||
'TikTok',
|
||||
'Spotify',
|
||||
'Twitch',
|
||||
'Linux',
|
||||
'Java',
|
||||
'JavaScript',
|
||||
'Python',
|
||||
'Ruby',
|
||||
'DotNet',
|
||||
'Kubernetes',
|
||||
'Docker',
|
||||
'Baidu',
|
||||
'HarmonyOS',
|
||||
];
|
||||
|
||||
const datum = [...direction, ...suggestion, ...editor, ...data, ...logo];
|
||||
|
||||
const other = all.filter((n) => !datum.includes(n));
|
||||
|
||||
export const categories = {
|
||||
direction,
|
||||
suggestion,
|
||||
editor,
|
||||
data,
|
||||
logo,
|
||||
other,
|
||||
};
|
||||
|
||||
export default categories;
|
||||
|
||||
export type Categories = typeof categories;
|
||||
export type CategoriesKeys = keyof Categories;
|
||||
@@ -1,61 +0,0 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { Skeleton } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
const IconSearch = React.lazy(() => import('./IconSearch'));
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
searchWrapper: css`
|
||||
display: flex;
|
||||
gap: ${cssVar.padding};
|
||||
> *:first-child {
|
||||
flex: 0 0 328px;
|
||||
}
|
||||
> *:last-child {
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
fallbackWrapper: css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
> * {
|
||||
flex: 0 0 15%;
|
||||
margin: ${cssVar.marginXXS} 0;
|
||||
}
|
||||
`,
|
||||
skeletonWrapper: css`
|
||||
text-align: center;
|
||||
|
||||
> * {
|
||||
width: 100% !important;
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
const IconSearchFallback: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.searchWrapper}>
|
||||
<Skeleton.Button active style={{ width: '100%', height: 40 }} />
|
||||
<Skeleton.Input active style={{ width: '100%', height: 40 }} />
|
||||
</div>
|
||||
<Skeleton.Button active style={{ margin: '28px 0 10px', width: 100 }} />
|
||||
<div className={styles.fallbackWrapper}>
|
||||
{Array.from({ length: 24 }).map((_, index) => (
|
||||
<div key={index} className={styles.skeletonWrapper}>
|
||||
<Skeleton.Node active style={{ height: 110, width: '100%' }}>
|
||||
{' '}
|
||||
</Skeleton.Node>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => (
|
||||
<Suspense fallback={<IconSearchFallback />}>
|
||||
<IconSearch />
|
||||
</Suspense>
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial, 'ledger'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial],
|
||||
category: 'logo',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: check,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: check,
|
||||
category: 'suggestion',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: ['time', 'watch', 'alarm'],
|
||||
category: 'suggestion',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis, 'feedback', 'discussion', 'reply', 'opinion', 'note'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: ['复制', 'clone', 'duplicate'],
|
||||
category: 'editor',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis],
|
||||
category: 'editor',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...check],
|
||||
category: 'suggestion',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis, 'vertical'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial, 'safety', 'protection', 'security', 'shield'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: ['ai'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...check, 'protection', 'security', 'shield', 'safety', 'privacy'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...check, 'time', 'clock', 'calendar'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
@@ -1,62 +0,0 @@
|
||||
import AccountBook from './AccountBook';
|
||||
import Alipay from './Alipay';
|
||||
import Bank from './Bank';
|
||||
import CarryOut from './CarryOut';
|
||||
import Check from './Check';
|
||||
import ClockSquare from './ClockSquare';
|
||||
import Comment from './Comment';
|
||||
import Copy from './Copy';
|
||||
import CreditCard from './CreditCard';
|
||||
import Dash from './Dash';
|
||||
import Dollar from './Dollar';
|
||||
import Ellipsis from './Ellipsis';
|
||||
import Euro from './Euro';
|
||||
import Holder from './Holder';
|
||||
import IssuesClose from './IssuesClose';
|
||||
import More from './More';
|
||||
import PayCircle from './PayCircle';
|
||||
import PoundCircle from './PoundCircle';
|
||||
import PropertySafety from './PropertySafety';
|
||||
import RedEnvelope from './RedEnvelope';
|
||||
import Robot from './Robot';
|
||||
import Safety from './Safety';
|
||||
import Schedule from './Schedule';
|
||||
import Transaction from './Transaction';
|
||||
|
||||
const all = {
|
||||
AccountBook,
|
||||
Alipay,
|
||||
AlipayCircle: Alipay,
|
||||
Bank,
|
||||
CarryOut,
|
||||
Check,
|
||||
CheckCircle: Check,
|
||||
CheckSquare: Check,
|
||||
ClockSquare,
|
||||
Comment,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Dash,
|
||||
SmallDash: Dash,
|
||||
Dollar,
|
||||
Ellipsis,
|
||||
Euro,
|
||||
EuroCircle: Euro,
|
||||
Holder,
|
||||
IssuesClose,
|
||||
More,
|
||||
PayCircle,
|
||||
PoundCircle,
|
||||
Pound: PoundCircle,
|
||||
PropertySafety,
|
||||
RedEnvelope,
|
||||
Robot,
|
||||
Safety,
|
||||
Schedule,
|
||||
Transaction,
|
||||
};
|
||||
|
||||
export type IconsMeta = typeof all;
|
||||
export type IconName = keyof IconsMeta;
|
||||
|
||||
export default all;
|
||||
@@ -1,87 +0,0 @@
|
||||
import type { CategoriesKeys } from '../fields';
|
||||
|
||||
export type IconMetaSchema = Readonly<{
|
||||
contributors: string[];
|
||||
tags: readonly string[];
|
||||
category: CategoriesKeys;
|
||||
}>;
|
||||
|
||||
export const check = [
|
||||
'check',
|
||||
'done',
|
||||
'todo',
|
||||
'tick',
|
||||
'complete',
|
||||
'finish',
|
||||
'task',
|
||||
|
||||
'ok',
|
||||
'success',
|
||||
'confirm',
|
||||
'approve',
|
||||
'agree',
|
||||
'validation',
|
||||
|
||||
'√',
|
||||
'✔',
|
||||
'✓',
|
||||
'勾',
|
||||
'对',
|
||||
'正确',
|
||||
'right',
|
||||
] as const;
|
||||
|
||||
export const financial = [
|
||||
'monetization',
|
||||
'marketing',
|
||||
'currency',
|
||||
'money',
|
||||
'payment',
|
||||
'finance',
|
||||
'cash',
|
||||
'bank',
|
||||
'transaction',
|
||||
'balance',
|
||||
'expense',
|
||||
'income',
|
||||
'budget',
|
||||
'investment',
|
||||
'savings',
|
||||
'profit',
|
||||
'cost',
|
||||
'wealth',
|
||||
'economy',
|
||||
'wallet',
|
||||
'exchange',
|
||||
] as const;
|
||||
|
||||
export const ellipsis = [
|
||||
'...',
|
||||
'。。。',
|
||||
'…',
|
||||
'more',
|
||||
'更多',
|
||||
'dots',
|
||||
'ellipsis',
|
||||
'expand',
|
||||
'collapse',
|
||||
'menu',
|
||||
'dropdown',
|
||||
'options',
|
||||
'settings',
|
||||
'et cetera',
|
||||
'etc',
|
||||
'loader',
|
||||
'loading',
|
||||
'progress',
|
||||
'pending',
|
||||
'throbber',
|
||||
'spinner',
|
||||
'operator',
|
||||
'code',
|
||||
'spread',
|
||||
'rest',
|
||||
'further',
|
||||
'extra',
|
||||
'overflow',
|
||||
] as const;
|
||||
@@ -1,48 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { CustomIconComponentProps } from '@ant-design/icons/es/components/Icon';
|
||||
|
||||
type CustomIconComponent = React.ComponentType<
|
||||
CustomIconComponentProps | React.SVGProps<SVGSVGElement>
|
||||
>;
|
||||
|
||||
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 (
|
||||
<svg {...props} viewBox="0 0 1024 1024">
|
||||
<title>Filled Icon</title>
|
||||
<path d={path} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<svg {...props} viewBox="0 0 1024 1024">
|
||||
<title>Outlined Icon</title>
|
||||
<path d={path} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<svg {...props} viewBox="0 0 1024 1024">
|
||||
<title>TwoTone Icon</title>
|
||||
<path d={path} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
import React from 'react';
|
||||
import { toArray } from '@rc-component/util';
|
||||
import { Image } from 'antd';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
export interface ImagePreviewProps {
|
||||
className?: string;
|
||||
/** Do not show padding & background */
|
||||
pure?: boolean;
|
||||
}
|
||||
|
||||
function isGood(className: string): boolean {
|
||||
return /\bgood\b/i.test(className);
|
||||
}
|
||||
|
||||
function isBad(className: string): boolean {
|
||||
return /\bbad\b/i.test(className);
|
||||
}
|
||||
|
||||
function isInline(className: string): boolean {
|
||||
return /\binline\b/i.test(className);
|
||||
}
|
||||
|
||||
function isGoodBadImg(imgMeta: any): boolean {
|
||||
return imgMeta.isGood || imgMeta.isBad;
|
||||
}
|
||||
|
||||
function isCompareImg(imgMeta: any): boolean {
|
||||
return isGoodBadImg(imgMeta) || imgMeta.inline;
|
||||
}
|
||||
|
||||
interface MateType {
|
||||
className: string;
|
||||
alt: string;
|
||||
description: string;
|
||||
src: string;
|
||||
isGood: boolean;
|
||||
isBad: boolean;
|
||||
inline: boolean;
|
||||
}
|
||||
|
||||
const ImagePreview: React.FC<React.PropsWithChildren<ImagePreviewProps>> = (props) => {
|
||||
const { children, className: rootClassName, pure } = props;
|
||||
|
||||
const imgs: React.ReactElement<any>[] = toArray(children).filter((ele) => ele.type === 'img');
|
||||
|
||||
const imgsMeta = imgs.map<Partial<MateType>>((img) => {
|
||||
const { alt, description, src, className } = img.props;
|
||||
return {
|
||||
className,
|
||||
alt,
|
||||
description,
|
||||
src,
|
||||
isGood: isGood(className),
|
||||
isBad: isBad(className),
|
||||
inline: isInline(className),
|
||||
};
|
||||
});
|
||||
|
||||
const imagesList = imgsMeta.map<React.ReactNode>((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} draggable={false} src={meta.src} alt={meta.alt} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const comparable =
|
||||
(imgs.length === 2 && imgsMeta.every(isCompareImg)) ||
|
||||
(imgs.length >= 2 && imgsMeta.every(isGoodBadImg));
|
||||
|
||||
const style: React.CSSProperties = comparable
|
||||
? { width: `${(100 / imgs.length).toFixed(3)}%` }
|
||||
: {};
|
||||
|
||||
const hasCarousel = imgs.length > 1 && !comparable;
|
||||
|
||||
const previewClassName = clsx(rootClassName, 'clearfix', 'preview-image-boxes', {
|
||||
'preview-image-boxes-compare': comparable,
|
||||
'preview-image-boxes-with-carousel': hasCarousel,
|
||||
});
|
||||
|
||||
// ===================== Render =====================
|
||||
const imgWrapperCls = 'preview-image-wrapper';
|
||||
|
||||
return (
|
||||
<div className={previewClassName}>
|
||||
{!imgs.length && (
|
||||
<div
|
||||
className={imgWrapperCls}
|
||||
style={pure ? { background: 'transparent', padding: 0 } : {}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{imagesList.map((_, index) => {
|
||||
if (!comparable && index !== 0) {
|
||||
return null;
|
||||
}
|
||||
const coverMeta = imgsMeta[index];
|
||||
const imageWrapperClassName = clsx(imgWrapperCls, {
|
||||
good: coverMeta.isGood,
|
||||
bad: coverMeta.isBad,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="preview-image-box" style={style} key={index}>
|
||||
<div className={imageWrapperClassName}>
|
||||
<Image className={coverMeta.className} src={coverMeta.src} alt={coverMeta.alt} />
|
||||
</div>
|
||||
<div className="preview-image-title">{coverMeta.alt}</div>
|
||||
<div
|
||||
className="preview-image-description"
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: it's for markdown
|
||||
dangerouslySetInnerHTML={{ __html: coverMeta.description ?? '' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImagePreview;
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import { PictureOutlined } from '@ant-design/icons';
|
||||
import { Image, Tooltip, Typography } from 'antd';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
tip: '预览',
|
||||
},
|
||||
en: {
|
||||
tip: 'Preview',
|
||||
},
|
||||
};
|
||||
|
||||
export interface InlinePopoverProps {
|
||||
previewURL?: string;
|
||||
}
|
||||
|
||||
// 鼠标悬浮弹出 Popover 组件,用于帮助用户更快看到一些属性对应的预览效果
|
||||
const InlinePopover: React.FC<InlinePopoverProps> = (props) => {
|
||||
const { previewURL } = props;
|
||||
const [locale] = useLocale(locales);
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={locale.tip}>
|
||||
<Typography.Link onClick={() => setVisible(true)}>
|
||||
<PictureOutlined />
|
||||
</Typography.Link>
|
||||
</Tooltip>
|
||||
|
||||
<Image
|
||||
width={10}
|
||||
style={{ display: 'none' }}
|
||||
src={previewURL}
|
||||
preview={{
|
||||
visible,
|
||||
src: previewURL,
|
||||
onVisibleChange: (value) => {
|
||||
setVisible(value);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InlinePopover;
|
||||
@@ -1,141 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
iconWrap: css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
vertical-align: -0.125em;
|
||||
`,
|
||||
}));
|
||||
|
||||
const BunIcon: React.FC<IconProps> = (props) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={clsx(classNames.iconWrap, className)} style={style}>
|
||||
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70" width="1em" height="1em">
|
||||
<title>Bun Logo</title>
|
||||
<path
|
||||
id="Shadow"
|
||||
d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"
|
||||
/>
|
||||
<g id="Body">
|
||||
<path
|
||||
id="Background"
|
||||
d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z"
|
||||
style={{
|
||||
fill: '#fbf0df',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
id="Bottom_Shadow"
|
||||
data-name="Bottom Shadow"
|
||||
d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z"
|
||||
style={{
|
||||
fill: '#f6dece',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
id="Light_Shine"
|
||||
data-name="Light Shine"
|
||||
d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z"
|
||||
style={{
|
||||
fill: '#fffefc',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
id="Top"
|
||||
d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z"
|
||||
style={{
|
||||
fill: '#ccbea7',
|
||||
fillRule: 'evenodd',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
id="Outline"
|
||||
d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"
|
||||
/>
|
||||
</g>
|
||||
<g id="Mouth">
|
||||
<g id="Background-2" data-name="Background">
|
||||
<path
|
||||
d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z"
|
||||
style={{
|
||||
fill: '#b71422',
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g id="Tongue">
|
||||
<path
|
||||
id="Background-3"
|
||||
data-name="Background"
|
||||
d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z"
|
||||
style={{
|
||||
fill: '#ff6164',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
id="Outline-2"
|
||||
data-name="Outline"
|
||||
d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
id="Outline-3"
|
||||
data-name="Outline"
|
||||
d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"
|
||||
/>
|
||||
</g>
|
||||
<g id="Face">
|
||||
<ellipse
|
||||
id="Right_Blush"
|
||||
data-name="Right Blush"
|
||||
cx="53.22"
|
||||
cy="40.18"
|
||||
rx="5.85"
|
||||
ry="3.44"
|
||||
style={{
|
||||
fill: '#febbd0',
|
||||
}}
|
||||
/>
|
||||
<ellipse
|
||||
id="Left_Bluch"
|
||||
data-name="Left Bluch"
|
||||
cx="22.95"
|
||||
cy="40.18"
|
||||
rx="5.85"
|
||||
ry="3.44"
|
||||
style={{
|
||||
fill: '#febbd0',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
id="Eyes"
|
||||
d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z"
|
||||
style={{
|
||||
fillRule: 'evenodd',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
id="Iris"
|
||||
d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z"
|
||||
style={{
|
||||
fill: '#fff',
|
||||
fillRule: 'evenodd',
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default BunIcon;
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ConfigProvider, Tabs } from 'antd';
|
||||
import SourceCode from 'dumi/theme-default/builtins/SourceCode';
|
||||
import type { Tab } from '@rc-component/tabs/lib/interface';
|
||||
|
||||
import BunLogo from './bun';
|
||||
import NpmLogo from './npm';
|
||||
import PnpmLogo from './pnpm';
|
||||
import YarnLogo from './yarn';
|
||||
|
||||
interface InstallProps {
|
||||
npm?: string;
|
||||
yarn?: string;
|
||||
pnpm?: string;
|
||||
bun?: string;
|
||||
}
|
||||
|
||||
const InstallDependencies: React.FC<InstallProps> = (props) => {
|
||||
const { npm, yarn, pnpm, bun } = props;
|
||||
const items: Tab[] = [
|
||||
{
|
||||
key: 'npm',
|
||||
label: 'npm',
|
||||
children: npm ? <SourceCode lang="bash">{npm}</SourceCode> : null,
|
||||
icon: <NpmLogo />,
|
||||
},
|
||||
{
|
||||
key: 'yarn',
|
||||
label: 'yarn',
|
||||
children: yarn ? <SourceCode lang="bash">{yarn}</SourceCode> : null,
|
||||
icon: <YarnLogo />,
|
||||
},
|
||||
{
|
||||
key: 'pnpm',
|
||||
label: 'pnpm',
|
||||
children: pnpm ? <SourceCode lang="bash">{pnpm}</SourceCode> : null,
|
||||
icon: <PnpmLogo />,
|
||||
},
|
||||
{
|
||||
key: 'bun',
|
||||
label: 'Bun',
|
||||
children: bun ? <SourceCode lang="bash">{bun}</SourceCode> : null,
|
||||
icon: <BunLogo />,
|
||||
},
|
||||
].filter((item) => item.children);
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={{ components: { Tabs: { horizontalMargin: '0' } } }}>
|
||||
<Tabs className="markdown" size="small" defaultActiveKey="npm" items={items} />
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstallDependencies;
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
iconWrap: css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
vertical-align: -0.125em;
|
||||
`,
|
||||
}));
|
||||
|
||||
const NpmIcon: React.FC<IconProps> = (props) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={clsx(classNames.iconWrap, className)} style={style}>
|
||||
<svg
|
||||
fill="#E53E3E"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
stroke="#E53E3E"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 16 16"
|
||||
width="1em"
|
||||
>
|
||||
<title>npm icon</title>
|
||||
<path d="M0 0v16h16v-16h-16zM13 13h-2v-8h-3v8h-5v-10h10v10z" />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default NpmIcon;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
iconWrap: css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
vertical-align: -0.125em;
|
||||
`,
|
||||
}));
|
||||
|
||||
const PnpmIcon: React.FC<IconProps> = (props) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={clsx(classNames.iconWrap, className)} style={style}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="#F69220"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="img"
|
||||
stroke="#F69220"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
>
|
||||
<title>pnpm icon</title>
|
||||
<path d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z" />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default PnpmIcon;
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
iconWrap: css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
vertical-align: -0.125em;
|
||||
`,
|
||||
}));
|
||||
|
||||
const YarnIcon: React.FC<IconProps> = (props) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={clsx(classNames.iconWrap, className)} style={style}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="#2C8EBB"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
stroke="#2C8EBB"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 496 512"
|
||||
width="1em"
|
||||
>
|
||||
<title>yarn icon</title>
|
||||
<path d="M393.9 345.2c-39 9.3-48.4 32.1-104 47.4 0 0-2.7 4-10.4 5.8-13.4 3.3-63.9 6-68.5 6.1-12.4.1-19.9-3.2-22-8.2-6.4-15.3 9.2-22 9.2-22-8.1-5-9-9.9-9.8-8.1-2.4 5.8-3.6 20.1-10.1 26.5-8.8 8.9-25.5 5.9-35.3.8-10.8-5.7.8-19.2.8-19.2s-5.8 3.4-10.5-3.6c-6-9.3-17.1-37.3 11.5-62-1.3-10.1-4.6-53.7 40.6-85.6 0 0-20.6-22.8-12.9-43.3 5-13.4 7-13.3 8.6-13.9 5.7-2.2 11.3-4.6 15.4-9.1 20.6-22.2 46.8-18 46.8-18s12.4-37.8 23.9-30.4c3.5 2.3 16.3 30.6 16.3 30.6s13.6-7.9 15.1-5c8.2 16 9.2 46.5 5.6 65.1-6.1 30.6-21.4 47.1-27.6 57.5-1.4 2.4 16.5 10 27.8 41.3 10.4 28.6 1.1 52.7 2.8 55.3.8 1.4 13.7.8 36.4-13.2 12.8-7.9 28.1-16.9 45.4-17 16.7-.5 17.6 19.2 4.9 22.2zM496 256c0 136.9-111.1 248-248 248S0 392.9 0 256 111.1 8 248 8s248 111.1 248 248zm-79.3 75.2c-1.7-13.6-13.2-23-28-22.8-22 .3-40.5 11.7-52.8 19.2-4.8 3-8.9 5.2-12.4 6.8 3.1-44.5-22.5-73.1-28.7-79.4 7.8-11.3 18.4-27.8 23.4-53.2 4.3-21.7 3-55.5-6.9-74.5-1.6-3.1-7.4-11.2-21-7.4-9.7-20-13-22.1-15.6-23.8-1.1-.7-23.6-16.4-41.4 28-12.2.9-31.3 5.3-47.5 22.8-2 2.2-5.9 3.8-10.1 5.4h.1c-8.4 3-12.3 9.9-16.9 22.3-6.5 17.4.2 34.6 6.8 45.7-17.8 15.9-37 39.8-35.7 82.5-34 36-11.8 73-5.6 79.6-1.6 11.1 3.7 19.4 12 23.8 12.6 6.7 30.3 9.6 43.9 2.8 4.9 5.2 13.8 10.1 30 10.1 6.8 0 58-2.9 72.6-6.5 6.8-1.6 11.5-4.5 14.6-7.1 9.8-3.1 36.8-12.3 62.2-28.7 18-11.7 24.2-14.2 37.6-17.4 12.9-3.2 21-15.1 19.4-28.2z" />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default YarnIcon;
|
||||
@@ -1,55 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Link } from 'dumi';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
|
||||
type LinkProps = Parameters<typeof Link>[0];
|
||||
|
||||
export interface LocaleLinkProps extends LinkProps {
|
||||
sourceType: 'a' | 'Link';
|
||||
}
|
||||
|
||||
const LocaleLink: React.FC<React.PropsWithChildren<LocaleLinkProps>> = ({
|
||||
sourceType,
|
||||
to,
|
||||
...props
|
||||
}) => {
|
||||
const Component = sourceType === 'a' ? 'a' : Link;
|
||||
|
||||
const [, localeType] = useLocale();
|
||||
|
||||
const localeTo = React.useMemo(() => {
|
||||
if (!to || typeof to !== 'string') {
|
||||
return to;
|
||||
}
|
||||
|
||||
// Auto locale switch
|
||||
const cells = to.match(/(\/[^#]*)(#.*)?/);
|
||||
if (cells) {
|
||||
let path = cells[1].replace(/\/$/, '');
|
||||
const hash = cells[2] || '';
|
||||
|
||||
if (localeType === 'cn' && !path.endsWith('-cn')) {
|
||||
path = `${path}-cn`;
|
||||
} else if (localeType === 'en' && path.endsWith('-cn')) {
|
||||
path = path.replace(/-cn$/, '');
|
||||
}
|
||||
|
||||
return `${path}${hash}`;
|
||||
}
|
||||
|
||||
return to;
|
||||
}, [localeType, to]);
|
||||
|
||||
const linkProps: LocaleLinkProps = {
|
||||
...props,
|
||||
} as LocaleLinkProps;
|
||||
|
||||
if (to) {
|
||||
linkProps.to = localeTo;
|
||||
}
|
||||
|
||||
return <Component {...linkProps} />;
|
||||
};
|
||||
|
||||
export default LocaleLink;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user