mirror of
https://github.com/ant-design/ant-design.git
synced 2026-02-09 10:59:19 +08:00
Compare commits
316 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03abfcbcbc | ||
|
|
b34dd64fbe | ||
|
|
353fd98d99 | ||
|
|
cee829816a | ||
|
|
19d7bfb1d5 | ||
|
|
38be7055d1 | ||
|
|
8e7ff042cd | ||
|
|
7cb9ca9336 | ||
|
|
114efa023b | ||
|
|
72c0b2c8ae | ||
|
|
e9005a6260 | ||
|
|
c9e3977d63 | ||
|
|
6b0387fe3d | ||
|
|
efb64f513f | ||
|
|
776aa384e8 | ||
|
|
827be4cceb | ||
|
|
1b36dafadc | ||
|
|
6ae303e972 | ||
|
|
ce4c939910 | ||
|
|
d6f58cbe28 | ||
|
|
b08961831d | ||
|
|
2950b7da98 | ||
|
|
038759b7ee | ||
|
|
8a16c49f5c | ||
|
|
bbb0683ab6 | ||
|
|
99eb877828 | ||
|
|
c7b3b38cbd | ||
|
|
b8b9938263 | ||
|
|
9673185fbf | ||
|
|
d2d124d305 | ||
|
|
8b3722f21f | ||
|
|
d5457c75f9 | ||
|
|
ee4220b676 | ||
|
|
6e91b7f8af | ||
|
|
d442e3e1a6 | ||
|
|
46fe122165 | ||
|
|
1a789f1e5b | ||
|
|
45d8f9a2d4 | ||
|
|
f44ec04609 | ||
|
|
3810d1dc6a | ||
|
|
ef32250465 | ||
|
|
d71e9ffb0d | ||
|
|
8d925cbb02 | ||
|
|
955afb14f4 | ||
|
|
443abf2ebe | ||
|
|
1931606b97 | ||
|
|
ceddbda113 | ||
|
|
41a5b2fe9e | ||
|
|
08f23516a0 | ||
|
|
5a0f141ddc | ||
|
|
3da7cf9033 | ||
|
|
b1bebbb9c3 | ||
|
|
20f8a9a9ed | ||
|
|
f20a3d6fba | ||
|
|
191db91774 | ||
|
|
a28f8aa863 | ||
|
|
4ab4a826c9 | ||
|
|
0c7ee7c6c5 | ||
|
|
98bc0f0756 | ||
|
|
7b323c5254 | ||
|
|
ffbf6e3024 | ||
|
|
4b9085624b | ||
|
|
7dd385bb43 | ||
|
|
44a4161dd8 | ||
|
|
10e43e1405 | ||
|
|
80b2250d6c | ||
|
|
93fb6c35e2 | ||
|
|
3e2b7546aa | ||
|
|
3073cf188d | ||
|
|
f78e800519 | ||
|
|
35cd37b3d8 | ||
|
|
a8b6e3d015 | ||
|
|
ebc17059de | ||
|
|
a93b4122ff | ||
|
|
8743710da7 | ||
|
|
3b43f2d8e5 | ||
|
|
9431db4dc1 | ||
|
|
ead5840d90 | ||
|
|
166e9c88e5 | ||
|
|
b2d14dd478 | ||
|
|
b02fe5018b | ||
|
|
076c84b181 | ||
|
|
350f73c89e | ||
|
|
920b24f328 | ||
|
|
50a3f30a72 | ||
|
|
5629062ba1 | ||
|
|
63e4d29760 | ||
|
|
4c12447bae | ||
|
|
94fe19feef | ||
|
|
f1c70f8c54 | ||
|
|
9df0d7656f | ||
|
|
dc8acf547d | ||
|
|
4655434949 | ||
|
|
c880c2e806 | ||
|
|
58c5fc4625 | ||
|
|
250597ea0c | ||
|
|
3034888500 | ||
|
|
7f1bd84097 | ||
|
|
705c3642d3 | ||
|
|
fbe7864e10 | ||
|
|
3c256d3f57 | ||
|
|
e5328f6464 | ||
|
|
2f6548209d | ||
|
|
6f90801861 | ||
|
|
b5b735a490 | ||
|
|
8bebba7af8 | ||
|
|
6a74e0bed5 | ||
|
|
1a15e8485e | ||
|
|
5854f71b5a | ||
|
|
ae0d69547a | ||
|
|
52ce51f2bf | ||
|
|
568a398bc8 | ||
|
|
e44f0063b1 | ||
|
|
3ac1fc78e7 | ||
|
|
f091c1e01b | ||
|
|
acc11dfccb | ||
|
|
964eb9d9d5 | ||
|
|
0b36e295f0 | ||
|
|
3ee9ff0811 | ||
|
|
092e37861a | ||
|
|
80dad184ee | ||
|
|
04dadedeb3 | ||
|
|
e0c9df2fed | ||
|
|
01fdc86b39 | ||
|
|
a3dcbdfae4 | ||
|
|
1897cfd126 | ||
|
|
756975004b | ||
|
|
4951f7e44c | ||
|
|
d5b3409660 | ||
|
|
8b4235a53b | ||
|
|
c46ce7fb86 | ||
|
|
8574b941a7 | ||
|
|
edf65d3eeb | ||
|
|
10d2f753f7 | ||
|
|
c5e5ac7c3f | ||
|
|
937f5d0738 | ||
|
|
d6d7e90998 | ||
|
|
3848b6bb82 | ||
|
|
bb5642f90b | ||
|
|
60442f9d44 | ||
|
|
41fd735434 | ||
|
|
c8e79dda70 | ||
|
|
2389eb8731 | ||
|
|
b9d23a1d96 | ||
|
|
3eecc366bb | ||
|
|
fecb9a04f6 | ||
|
|
77f0f1a9fe | ||
|
|
aeff7cab63 | ||
|
|
5df2c59516 | ||
|
|
247c41c79b | ||
|
|
18ed518fdd | ||
|
|
4dc35000b1 | ||
|
|
c33138840f | ||
|
|
a9e9c04cbc | ||
|
|
4f61124be4 | ||
|
|
fccc092154 | ||
|
|
a680d84865 | ||
|
|
f7446f3f1f | ||
|
|
28d674445b | ||
|
|
4ab542e7c3 | ||
|
|
07f0db2ddc | ||
|
|
2e51288638 | ||
|
|
258573ab93 | ||
|
|
3c9bb681e7 | ||
|
|
3e0b56dacf | ||
|
|
7d35e112ae | ||
|
|
6e55573b5c | ||
|
|
0e4f60dd01 | ||
|
|
0b55335da9 | ||
|
|
40c0bd6bb1 | ||
|
|
98614197cb | ||
|
|
8fd30ab4a2 | ||
|
|
e05aa23faa | ||
|
|
050d1dfa42 | ||
|
|
d90f8ed238 | ||
|
|
185398afdc | ||
|
|
b3adc9a69b | ||
|
|
fbd6fdafbe | ||
|
|
5b9b6f801a | ||
|
|
27826a8136 | ||
|
|
4f53033269 | ||
|
|
c235b1193b | ||
|
|
ff35ebb285 | ||
|
|
0d603d44fa | ||
|
|
2a8a811762 | ||
|
|
cb309acd4a | ||
|
|
e2ce447c47 | ||
|
|
f62d50533b | ||
|
|
e15b8defc9 | ||
|
|
e33444368e | ||
|
|
4a88f6a98a | ||
|
|
f324de2173 | ||
|
|
0362603ae5 | ||
|
|
8def94703b | ||
|
|
67a0e2fee7 | ||
|
|
584a7be26f | ||
|
|
3f5129ac44 | ||
|
|
d937a3930f | ||
|
|
c300c97a3e | ||
|
|
ec953cbffa | ||
|
|
caccb11d6f | ||
|
|
a863062f7b | ||
|
|
584923d35b | ||
|
|
75ddcdfd00 | ||
|
|
9b2054ffec | ||
|
|
b1bb15a753 | ||
|
|
c78056d0a4 | ||
|
|
ba47850fa0 | ||
|
|
ae98485a3e | ||
|
|
9bb27c4100 | ||
|
|
64f7963395 | ||
|
|
6fac5c24bf | ||
|
|
ee8cc27686 | ||
|
|
364bcc74d1 | ||
|
|
7130056493 | ||
|
|
8a4e89fc59 | ||
|
|
6893402469 | ||
|
|
957b027fa1 | ||
|
|
6c5036e285 | ||
|
|
4888842fc4 | ||
|
|
1463722eb5 | ||
|
|
c3710c0102 | ||
|
|
ae9fc640c6 | ||
|
|
1a3b29ec8f | ||
|
|
8ac5c5fde4 | ||
|
|
d7aac85735 | ||
|
|
0cdfaf578a | ||
|
|
c4b105c66a | ||
|
|
1e9b8a997c | ||
|
|
82622f14d5 | ||
|
|
f7a216aaa9 | ||
|
|
a6d83589db | ||
|
|
a01c1293cf | ||
|
|
1721a1af9b | ||
|
|
8e1bdc8121 | ||
|
|
283b2822e0 | ||
|
|
d353c2949c | ||
|
|
1ec7cd744d | ||
|
|
50dc6acbb2 | ||
|
|
cab5910713 | ||
|
|
fbe0a46857 | ||
|
|
8af72472fa | ||
|
|
8ec34d6204 | ||
|
|
7e2f8ce1f0 | ||
|
|
9123bbbfd7 | ||
|
|
24bd6fd311 | ||
|
|
d9eb4ccb4b | ||
|
|
da8e1ccde2 | ||
|
|
89fa656149 | ||
|
|
8399590542 | ||
|
|
e2f3cf6133 | ||
|
|
5471db8383 | ||
|
|
d188f03bb8 | ||
|
|
edb2bde901 | ||
|
|
8fbae86149 | ||
|
|
fba5f4f6cb | ||
|
|
2ccab1e984 | ||
|
|
acd131b7c4 | ||
|
|
353a94f43d | ||
|
|
09b1107c35 | ||
|
|
3abfb8c2dc | ||
|
|
38efd012a2 | ||
|
|
49c4f56100 | ||
|
|
8852b23742 | ||
|
|
f160384640 | ||
|
|
1c5b04e47b | ||
|
|
330c5be52e | ||
|
|
eac8dde1e5 | ||
|
|
3a78aacb3c | ||
|
|
2ff9d89b15 | ||
|
|
3d152a78fd | ||
|
|
0a89f9cf21 | ||
|
|
9af831fd92 | ||
|
|
3686df953a | ||
|
|
d08de4e1de | ||
|
|
ec0bdd2984 | ||
|
|
6ed7b0f045 | ||
|
|
44abdc4cda | ||
|
|
b8b365c2d1 | ||
|
|
0774e5a40a | ||
|
|
a3d31746e3 | ||
|
|
7326f7b119 | ||
|
|
886a1d19cd | ||
|
|
5235b4fe32 | ||
|
|
5c52fea0bf | ||
|
|
39b7edd337 | ||
|
|
dbf71b5059 | ||
|
|
60cd020a35 | ||
|
|
9108b85cf7 | ||
|
|
1341d29312 | ||
|
|
397cb98bd1 | ||
|
|
56ff32425e | ||
|
|
8f616c018e | ||
|
|
a2aadb3fa8 | ||
|
|
5b8fd49fba | ||
|
|
fb6d7405d0 | ||
|
|
0d0f88c71a | ||
|
|
9a62d148c7 | ||
|
|
5efbb093bc | ||
|
|
ea4d320241 | ||
|
|
2ffd8a7012 | ||
|
|
81e7f3fe98 | ||
|
|
dfd962d350 | ||
|
|
742ecee166 | ||
|
|
d680ea3d30 | ||
|
|
c0589e939d | ||
|
|
d1b47edff4 | ||
|
|
631ee77e5b | ||
|
|
c3f41c19ba | ||
|
|
60efb8e170 | ||
|
|
9f45cb32c2 | ||
|
|
97ef4e304e | ||
|
|
1004fa28ea | ||
|
|
af27ca20fb | ||
|
|
bc1db71a0d | ||
|
|
7a919eb971 |
@@ -7,7 +7,6 @@ const isNumber = (value: any): value is number => {
|
||||
};
|
||||
|
||||
const fetcher = async (url: string): Promise<number> => {
|
||||
// eslint-disable-next-line compat/compat
|
||||
const res = await fetch(url, { headers: { Accept: 'application/vnd.github+json' } });
|
||||
const data = await res.json();
|
||||
const totalCount = isNumber(data?.total_count) ? data.total_count : 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Flex, Tag, version } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
import { useFullSidebarData, useSidebarData } from 'dumi';
|
||||
|
||||
@@ -12,29 +12,28 @@ import useLocation from './useLocation';
|
||||
const locales = {
|
||||
cn: {
|
||||
deprecated: '废弃',
|
||||
update: '更新',
|
||||
updated: '更新',
|
||||
new: '新增',
|
||||
},
|
||||
en: {
|
||||
deprecated: 'DEPRECATED',
|
||||
update: 'UPDATE',
|
||||
updated: 'UPDATED',
|
||||
new: 'NEW',
|
||||
},
|
||||
};
|
||||
|
||||
const getTagColor = (val?: string) => {
|
||||
switch (val?.toUpperCase()) {
|
||||
case 'UPDATE':
|
||||
case 'UPDATED':
|
||||
return 'processing';
|
||||
case 'DEPRECATED':
|
||||
return 'red';
|
||||
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
link: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -47,7 +46,7 @@ const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
font-weight: normal;
|
||||
font-size: ${cssVar.fontSizeSM};
|
||||
opacity: 0.8;
|
||||
margin-left: 4px;
|
||||
margin-inline-start: ${cssVar.marginSM};
|
||||
`,
|
||||
}));
|
||||
|
||||
@@ -63,10 +62,10 @@ interface MenuItemLabelProps {
|
||||
}
|
||||
|
||||
const MenuItemLabelWithTag: React.FC<MenuItemLabelProps> = (props) => {
|
||||
const { styles } = useStyle();
|
||||
const { before, after, link, title, subtitle, search, tag, className } = props;
|
||||
|
||||
const [locale] = useLocale(locales);
|
||||
|
||||
const getLocale = (name: string) => {
|
||||
return (locale as any)[name.toLowerCase()] ?? name;
|
||||
};
|
||||
@@ -74,7 +73,7 @@ const MenuItemLabelWithTag: React.FC<MenuItemLabelProps> = (props) => {
|
||||
if (!before && !after) {
|
||||
return (
|
||||
<Link to={`${link}${search}`} className={clsx(className, { [styles.link]: tag })}>
|
||||
<Flex justify="flex-start" align="center" gap="small">
|
||||
<Flex justify="flex-start" align="center">
|
||||
<span>{title}</span>
|
||||
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
||||
</Flex>
|
||||
|
||||
@@ -3,10 +3,14 @@ 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: none;
|
||||
animation: keepAlive ${duration}s linear;
|
||||
animation-fill-mode: forwards;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
@@ -50,7 +54,7 @@ const useThemeAnimation = () => {
|
||||
clipPath: isDark ? [...clipPath].reverse() : clipPath,
|
||||
},
|
||||
{
|
||||
duration: 500,
|
||||
duration: duration * 1000,
|
||||
easing: 'ease-in',
|
||||
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)',
|
||||
},
|
||||
|
||||
3
.dumi/layer-import.less
vendored
Normal file
3
.dumi/layer-import.less
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
@layer antd {
|
||||
@import '~../components/style/antd.css';
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import raf from '@rc-component/util/lib/raf';
|
||||
import { Alert, Badge, Carousel, Flex, Skeleton, Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
@@ -18,12 +19,46 @@ const useStyle = createStyles(({ cssVar, css, cx }) => {
|
||||
align-items: stretch;
|
||||
text-decoration: none;
|
||||
background: ${cssVar.colorBgContainer};
|
||||
background: color-mix(in srgb, ${cssVar.colorBgContainer} 30%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
border: ${cssVar.lineWidth} solid ${cssVar.colorBorderSecondary};
|
||||
border-radius: ${cssVar.borderRadiusLG};
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
padding-block: ${cssVar.paddingMD};
|
||||
padding-inline: ${cssVar.paddingLG};
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
inset: calc(${cssVar.lineWidth} * -1);
|
||||
position: absolute;
|
||||
|
||||
background: radial-gradient(
|
||||
circle 150px at var(--mouse-x, 0) var(--mouse-y, 0),
|
||||
${cssVar.colorPrimaryBorderHover},
|
||||
${cssVar.colorBorderSecondary}
|
||||
);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
|
||||
mask-composite: subtract;
|
||||
-webkit-mask-composite: xor;
|
||||
padding: 1px;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
backdrop-filter: blur(0px);
|
||||
background: color-mix(in srgb, ${cssVar.colorBgContainer} 90%, transparent);
|
||||
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
@@ -33,12 +68,6 @@ const useStyle = createStyles(({ cssVar, css, cx }) => {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
cardItem: css`
|
||||
&:hover {
|
||||
box-shadow: ${cssVar.boxShadowCard};
|
||||
border-color: transparent;
|
||||
}
|
||||
`,
|
||||
sliderItem: css`
|
||||
margin: 0 ${cssVar.margin};
|
||||
text-align: start;
|
||||
@@ -64,6 +93,9 @@ const useStyle = createStyles(({ cssVar, css, cx }) => {
|
||||
};
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// == Item ==
|
||||
// ======================================================================
|
||||
interface RecommendItemProps {
|
||||
extra: Extra;
|
||||
index: number;
|
||||
@@ -73,9 +105,47 @@ interface RecommendItemProps {
|
||||
|
||||
const RecommendItem: React.FC<RecommendItemProps> = (props) => {
|
||||
const { extra, index, icons, className } = props;
|
||||
const cardRef = React.useRef<HTMLAnchorElement>(null);
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
// ====================== MousePos ======================
|
||||
const [mousePosition, setMousePosition] = React.useState<[number, number]>([0, 0]);
|
||||
const [transMousePosition, setTransMousePosition] = React.useState<[number, number]>([0, 0]);
|
||||
|
||||
const onMouseMove = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
setMousePosition([x, y]);
|
||||
};
|
||||
|
||||
// Transition mouse position
|
||||
React.useEffect(() => {
|
||||
const [targetX, targetY] = mousePosition;
|
||||
const [currentX, currentY] = transMousePosition;
|
||||
|
||||
if (Math.abs(targetX - currentX) < 0.5 && Math.abs(targetY - currentY) < 0.5) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rafId = raf(() => {
|
||||
setTransMousePosition((ori) => {
|
||||
const [curX, curY] = ori;
|
||||
const deltaX = (targetX - curX) * 0.1;
|
||||
const deltaY = (targetY - curY) * 0.1;
|
||||
|
||||
return [curX + deltaX, curY + deltaY];
|
||||
});
|
||||
});
|
||||
|
||||
return () => raf.cancel(rafId);
|
||||
}, [mousePosition, transMousePosition]);
|
||||
|
||||
// ======================= Render =======================
|
||||
if (!extra) {
|
||||
return <Skeleton key={index} />;
|
||||
}
|
||||
@@ -84,11 +154,19 @@ const RecommendItem: React.FC<RecommendItemProps> = (props) => {
|
||||
|
||||
const card = (
|
||||
<a
|
||||
ref={cardRef}
|
||||
key={extra?.title}
|
||||
href={extra.href}
|
||||
target="_blank"
|
||||
className={clsx(styles.itemBase, className)}
|
||||
style={
|
||||
{
|
||||
'--mouse-x': `${transMousePosition[0]}px`,
|
||||
'--mouse-y': `${transMousePosition[1]}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
rel="noreferrer"
|
||||
onMouseMove={onMouseMove}
|
||||
>
|
||||
<Typography.Title level={5}>{extra?.title}</Typography.Title>
|
||||
<Typography.Paragraph type="secondary" style={{ flex: 'auto' }}>
|
||||
@@ -114,6 +192,9 @@ const RecommendItem: React.FC<RecommendItemProps> = (props) => {
|
||||
return card;
|
||||
};
|
||||
|
||||
// ======================================================================
|
||||
// == Fallback ==
|
||||
// ======================================================================
|
||||
export const BannerRecommendsFallback: React.FC = () => {
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
|
||||
@@ -140,6 +221,9 @@ export const BannerRecommendsFallback: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// ======================================================================
|
||||
// == Recommends ==
|
||||
// ======================================================================
|
||||
const BannerRecommends: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
const [, lang] = useLocale();
|
||||
@@ -186,13 +270,7 @@ const BannerRecommends: React.FC = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{mergedExtras.map((extra, index) => (
|
||||
<RecommendItem
|
||||
key={`desktop-${index}`}
|
||||
extra={extra}
|
||||
index={index}
|
||||
icons={data?.icons}
|
||||
className={styles.cardItem}
|
||||
/>
|
||||
<RecommendItem key={`desktop-${index}`} extra={extra} index={index} icons={data?.icons} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { CustomerServiceOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
Carousel,
|
||||
DatePicker,
|
||||
Flex,
|
||||
FloatButton,
|
||||
Modal,
|
||||
Progress,
|
||||
Masonry,
|
||||
Splitter,
|
||||
Tag,
|
||||
Tour,
|
||||
Typography,
|
||||
@@ -21,7 +21,6 @@ import SiteContext from '../../../theme/slots/SiteContext';
|
||||
import { DarkContext } from './../../../hooks/useDark';
|
||||
import { getCarouselStyle } from './util';
|
||||
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: ModalDoNotUseOrYouWillBeFired } = Modal;
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: DatePickerDoNotUseOrYouWillBeFired } = DatePicker;
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: TourDoNotUseOrYouWillBeFired } = Tour;
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: FloatButtonDoNotUseOrYouWillBeFired } = FloatButton;
|
||||
@@ -148,24 +147,26 @@ 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: 'Modal',
|
||||
// type: 'update',
|
||||
// node: (
|
||||
// <ModalDoNotUseOrYouWillBeFired title="Ant Design" width={300}>
|
||||
// {locale.sampleContent}
|
||||
// </ModalDoNotUseOrYouWillBeFired>
|
||||
// ),
|
||||
// },
|
||||
|
||||
{
|
||||
title: 'DatePicker',
|
||||
type: 'update',
|
||||
node: (
|
||||
<DatePickerDoNotUseOrYouWillBeFired
|
||||
value={dayjs('2022-11-18 14:00:00')}
|
||||
value={dayjs('2025-11-22 00:00:00')}
|
||||
// defaultValue={dayjs('2025-11-22 00:00:00')}
|
||||
showToday={false}
|
||||
presets={
|
||||
isMobile
|
||||
@@ -180,30 +181,30 @@ const ComponentsList: React.FC = () => {
|
||||
/>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// 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: '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: 'new',
|
||||
type: 'update',
|
||||
node: (
|
||||
<TourDoNotUseOrYouWillBeFired
|
||||
title="Ant Design"
|
||||
@@ -214,9 +215,10 @@ const ComponentsList: React.FC = () => {
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: 'FloatButton',
|
||||
type: 'new',
|
||||
type: 'update',
|
||||
node: (
|
||||
<Flex align="center" gap="large">
|
||||
<FloatButtonDoNotUseOrYouWillBeFired
|
||||
@@ -246,19 +248,83 @@ const ComponentsList: React.FC = () => {
|
||||
// },
|
||||
|
||||
{
|
||||
title: 'Alert',
|
||||
type: 'update',
|
||||
title: 'Splitter',
|
||||
type: 'new',
|
||||
node: (
|
||||
<Alert
|
||||
style={{ width: 400 }}
|
||||
title="Ant Design"
|
||||
description={locale.sampleContent}
|
||||
closable={{ closeIcon: true, disabled: true }}
|
||||
<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,
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { createStyles, useTheme } from 'antd-style';
|
||||
import { createStaticStyles, useTheme } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import SiteContext from '../../../theme/slots/SiteContext';
|
||||
import GroupMaskLayer from './GroupMaskLayer';
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
box: css`
|
||||
position: relative;
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
background-size: cover;
|
||||
background-position: 50% 0%;
|
||||
background-repeat: no-repeat;
|
||||
`,
|
||||
container: css`
|
||||
position: absolute;
|
||||
@@ -41,15 +44,46 @@ export interface GroupProps {
|
||||
/** 是否不使用两侧 margin */
|
||||
collapse?: boolean;
|
||||
decoration?: React.ReactNode;
|
||||
/** 预加载的背景图片列表 */
|
||||
backgroundPrefetchList?: string[];
|
||||
}
|
||||
|
||||
const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
|
||||
const { id, title, titleColor, description, children, decoration, background, collapse } = props;
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
titleColor,
|
||||
description,
|
||||
children,
|
||||
decoration,
|
||||
background,
|
||||
collapse,
|
||||
backgroundPrefetchList,
|
||||
} = props;
|
||||
|
||||
// 预加载背景图片
|
||||
React.useEffect(() => {
|
||||
if (backgroundPrefetchList && backgroundPrefetchList.length > 0) {
|
||||
backgroundPrefetchList.forEach((url) => {
|
||||
if (url && url.startsWith('https')) {
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [backgroundPrefetchList]);
|
||||
|
||||
const token = useTheme();
|
||||
const { styles } = useStyle();
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
return (
|
||||
<div style={{ backgroundColor: background }} className={styles.box}>
|
||||
<div
|
||||
style={
|
||||
background?.startsWith('https')
|
||||
? { backgroundImage: `url(${background})` }
|
||||
: { backgroundColor: background }
|
||||
}
|
||||
className={styles.box}
|
||||
>
|
||||
<div className={styles.container}>{decoration}</div>
|
||||
<GroupMaskLayer style={{ paddingBlock: token.marginFarSM }}>
|
||||
<div className={styles.typographyWrapper}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
const useStyle = createStyles(({ css }) => ({
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
siteMask: css`
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
@@ -19,11 +19,10 @@ export interface GroupMaskLayerProps {
|
||||
|
||||
const GroupMaskLayer: React.FC<React.PropsWithChildren<GroupMaskLayerProps>> = (props) => {
|
||||
const { children, className, style, onMouseMove, onMouseEnter, onMouseLeave } = props;
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={clsx(className, styles.siteMask)}
|
||||
className={clsx(className, classNames.siteMask)}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
import useLocale from '../../../../hooks/useLocale';
|
||||
import Tilt from './Tilt';
|
||||
@@ -71,7 +71,7 @@ const locales = {
|
||||
},
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => {
|
||||
const styles = createStaticStyles(({ cssVar, css }) => {
|
||||
const gap = cssVar.padding;
|
||||
return {
|
||||
holder: css`
|
||||
@@ -106,7 +106,6 @@ const useStyle = createStyles(({ cssVar, css }) => {
|
||||
|
||||
const ComponentsBlock: React.FC = () => {
|
||||
const [locale] = useLocale(locales);
|
||||
const { styles } = useStyle();
|
||||
|
||||
return (
|
||||
<Tilt options={{ max: 4, glare: false, scale: 0.98 }} className={styles.holder}>
|
||||
|
||||
115
.dumi/pages/index/components/PreviewBanner/LuminousBg.tsx
Normal file
115
.dumi/pages/index/components/PreviewBanner/LuminousBg.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEvent } from '@rc-component/util';
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
import { DarkContext } from '../../../../hooks/useDark';
|
||||
|
||||
interface BubbleProps {
|
||||
size: number | string;
|
||||
left?: number | string;
|
||||
top?: number | string;
|
||||
color: string;
|
||||
offsetXMultiple?: number;
|
||||
offsetYMultiple?: number;
|
||||
defaultOpacity?: number;
|
||||
}
|
||||
|
||||
const MAX_OFFSET = 200;
|
||||
|
||||
const Bubble = ({
|
||||
size,
|
||||
left,
|
||||
top,
|
||||
color,
|
||||
offsetXMultiple = 1,
|
||||
offsetYMultiple = 1,
|
||||
defaultOpacity = 0.1,
|
||||
}: BubbleProps) => {
|
||||
const [offset, setOffset] = useState([0, 0]);
|
||||
const [opacity, setOpacity] = useState(defaultOpacity);
|
||||
const [sizeOffset, setSizeOffset] = useState(1);
|
||||
|
||||
const isDark = React.use(DarkContext);
|
||||
|
||||
const randomPos = useEvent(() => {
|
||||
const baseOffsetX = (Math.random() - 0.5) * MAX_OFFSET * 2 * offsetXMultiple;
|
||||
const baseOffsetY = (Math.random() - 0.5) * MAX_OFFSET * 2 * offsetYMultiple;
|
||||
setOffset([baseOffsetX, baseOffsetY]);
|
||||
|
||||
setOpacity(isDark ? 0.1 + Math.random() * 0.2 : 0.1 + Math.random() * 0.05);
|
||||
setSizeOffset(1 + Math.random() * 1);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
randomPos();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const randomTimeout = Math.random() * 2000 + 3000;
|
||||
const id = setTimeout(randomPos, randomTimeout);
|
||||
|
||||
return () => clearTimeout(id);
|
||||
}, [offset]);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-desc="luminous-bubble"
|
||||
style={{
|
||||
opacity,
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '50%',
|
||||
background: color,
|
||||
filter: 'blur(100px)',
|
||||
left,
|
||||
top,
|
||||
transform: `translate(-50%, -50%) translate(${offset[0]}px, ${offset[1]}px) scale(${sizeOffset})`,
|
||||
transition: 'all 5s ease-in-out',
|
||||
position: 'absolute',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: ${cssVar.colorBgContainer};
|
||||
`,
|
||||
}));
|
||||
|
||||
interface LuminousBgProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function LuminousBg({ className }: LuminousBgProps) {
|
||||
const { styles, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container, className)}>
|
||||
{/* Left + Top */}
|
||||
<Bubble
|
||||
size={300}
|
||||
color="#ee35f1"
|
||||
left="0vw"
|
||||
top="0vh"
|
||||
offsetXMultiple={2}
|
||||
defaultOpacity={0.2}
|
||||
/>
|
||||
{/* Left + Bottom */}
|
||||
<Bubble size={300} color="#5939dc" left="30vw" top="80vh" defaultOpacity={0.1} />
|
||||
{/* Right + Middle */}
|
||||
<Bubble
|
||||
size={300}
|
||||
color="#00D6FF"
|
||||
left="100vw"
|
||||
top="50vh"
|
||||
offsetYMultiple={2}
|
||||
defaultOpacity={0.2}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +1,34 @@
|
||||
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'));
|
||||
import LuminousBg from './LuminousBg';
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
slogan: '助力设计开发者「更灵活」地搭建出「更美」的产品,让用户「快乐工作」~',
|
||||
slogan: 'AI 友好的「设计系统」,让美与智能并进,让工作充满「灵感」与「快乐」。',
|
||||
start: '开始使用',
|
||||
designLanguage: '设计语言',
|
||||
},
|
||||
en: {
|
||||
slogan:
|
||||
'Help designers/developers building beautiful products more flexible and working with happiness',
|
||||
'AI friendly design system that combines beauty and intelligence, making work full of inspiration and joy.',
|
||||
start: 'Getting Started',
|
||||
designLanguage: 'Design Language',
|
||||
},
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css, cx }, siteConfig: SiteContextProps) => {
|
||||
const useStyle = createStyles(({ cssVar, css, cx }) => {
|
||||
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;
|
||||
@@ -66,18 +52,12 @@ const useStyle = createStyles(({ cssVar, css, cx }, siteConfig: SiteContextProps
|
||||
row-gap: ${cssVar.marginXL};
|
||||
|
||||
&:hover {
|
||||
.${mask} {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.${block} {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
mask,
|
||||
|
||||
typography: css`
|
||||
text-align: center;
|
||||
position: relative;
|
||||
@@ -86,14 +66,15 @@ const useStyle = createStyles(({ cssVar, css, cx }, siteConfig: SiteContextProps
|
||||
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;
|
||||
font-size: calc(${cssVar.fontSizeHeading1} * 2) !important;
|
||||
line-height: ${cssVar.lineHeightHeading1} !important;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: ${cssVar.fontSizeLG} !important;
|
||||
font-weight: normal !important;
|
||||
font-size: calc(${cssVar.fontSizeLG} * 1.5) !important;
|
||||
font-weight: 400 !important;
|
||||
margin-bottom: 0;
|
||||
color: ${cssVar.colorTextTertiary} !important;
|
||||
}
|
||||
`,
|
||||
block,
|
||||
@@ -107,18 +88,6 @@ const useStyle = createStyles(({ cssVar, css, cx }, siteConfig: SiteContextProps
|
||||
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%'};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -132,31 +101,11 @@ const PreviewBanner: React.FC<Readonly<React.PropsWithChildren>> = (props) => {
|
||||
|
||||
return (
|
||||
<GroupMaskLayer>
|
||||
{/* Image Left Top */}
|
||||
<img
|
||||
alt="bg"
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/49f963db-b2a8-4f15-857a-270d771a1204.svg"
|
||||
draggable={false}
|
||||
className={clsx(styles.bgImg, styles.bgImgTop)}
|
||||
/>
|
||||
{/* Image Right Top */}
|
||||
<img
|
||||
alt="bg"
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/e152223c-bcae-4913-8938-54fda9efe330.svg"
|
||||
draggable={false}
|
||||
className={clsx(styles.bgImg, styles.bgImgBottom)}
|
||||
/>
|
||||
|
||||
<div className={styles.holder}>
|
||||
{/* Mobile not show the component preview */}
|
||||
<Suspense fallback={null}>
|
||||
{siteConfig.isMobile ? null : (
|
||||
<div className={styles.block}>
|
||||
<ComponentsBlock />
|
||||
</div>
|
||||
)}
|
||||
<LuminousBg />
|
||||
</Suspense>
|
||||
<div className={styles.mask} />
|
||||
<Typography className={styles.typography}>
|
||||
<h1>Ant Design</h1>
|
||||
<p>{locale.slogan}</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { CSSMotionList } from '@rc-component/motion';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import { COLOR_IMAGES, getClosetColor } from './colorUtil';
|
||||
@@ -10,7 +10,7 @@ export interface BackgroundImageProps {
|
||||
isLight?: boolean;
|
||||
}
|
||||
|
||||
const useStyle = createStyles(({ cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
image: css`
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
position: absolute;
|
||||
@@ -29,7 +29,6 @@ const onHide = () => ({ opacity: 0 });
|
||||
|
||||
const BackgroundImage: React.FC<BackgroundImageProps> = ({ colorPrimary, isLight }) => {
|
||||
const activeColor = useMemo(() => getClosetColor(colorPrimary), [colorPrimary]);
|
||||
const { styles } = useStyle();
|
||||
|
||||
const [keyList, setKeyList] = useState<string[]>([]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ColorPicker, Flex, Input } from 'antd';
|
||||
import type { ColorPickerProps, GetProp } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { generateColor } from 'antd/es/color-picker/util';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { PRESET_COLORS } from './colorUtil';
|
||||
|
||||
type Color = Extract<GetProp<ColorPickerProps, 'value'>, string | { cleared: any }>;
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
color: css`
|
||||
width: calc(${cssVar.controlHeightLG} / 2);
|
||||
height: calc(${cssVar.controlHeightLG} / 2);
|
||||
@@ -69,8 +69,6 @@ const DebouncedColorPicker: React.FC<React.PropsWithChildren<ThemeColorPickerPro
|
||||
};
|
||||
|
||||
const ThemeColorPicker: React.FC<ThemeColorPickerProps> = ({ value, onChange, id }) => {
|
||||
const { styles } = useStyle();
|
||||
|
||||
const matchColors = React.useMemo(() => {
|
||||
const valueStr = generateColor(value || '').toRgbString();
|
||||
const colors = PRESET_COLORS.map((color) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import useLocale from '../../../../hooks/useLocale';
|
||||
@@ -32,7 +32,7 @@ const locales = {
|
||||
},
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
themeCard: css`
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
cursor: pointer;
|
||||
@@ -80,7 +80,6 @@ export interface ThemePickerProps {
|
||||
|
||||
const ThemePicker: React.FC<ThemePickerProps> = (props) => {
|
||||
const { value, id, onChange } = props;
|
||||
const { styles } = useStyle();
|
||||
const [locale] = useLocale(locales);
|
||||
return (
|
||||
<Flex gap="large" wrap>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
theme,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { generateColor } from 'antd/es/color-picker/util';
|
||||
import { clsx } from 'clsx';
|
||||
import { useLocation } from 'dumi';
|
||||
@@ -92,7 +92,7 @@ const locales = {
|
||||
};
|
||||
|
||||
// ============================= Style =============================
|
||||
const useStyle = createStyles(({ cssVar, css, cx }) => {
|
||||
const styles = createStaticStyles(({ cssVar, css, cx }) => {
|
||||
const { carousel } = getCarouselStyle();
|
||||
const demo = css`
|
||||
overflow: hidden;
|
||||
@@ -345,7 +345,6 @@ function rgbToColorMatrix(color: string) {
|
||||
}
|
||||
|
||||
const Theme: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
const [locale, lang] = useLocale(locales);
|
||||
const isZhCN = lang === 'cn';
|
||||
const { search } = useLocation();
|
||||
|
||||
198
.dumi/pages/index/components/ThemePreview/ComponentsBlock.tsx
Normal file
198
.dumi/pages/index/components/ThemePreview/ComponentsBlock.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React from 'react';
|
||||
import { CheckOutlined, CloseOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
ColorPicker,
|
||||
ConfigProvider,
|
||||
Dropdown,
|
||||
Flex,
|
||||
Modal,
|
||||
Progress,
|
||||
Radio,
|
||||
Select,
|
||||
Slider,
|
||||
Space,
|
||||
Steps,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import useLocale from '../../../../hooks/useLocale';
|
||||
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: ModalPanel } = Modal;
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
range: '设置范围',
|
||||
text: 'Ant Design 使用 CSS-in-JS 技术以提供动态与混合主题的能力。与此同时,我们使用组件级别的 CSS-in-JS 解决方案,让你的应用获得更好的性能。',
|
||||
infoText: '信息内容展示',
|
||||
dropdown: '下拉菜单',
|
||||
finished: '已完成',
|
||||
inProgress: '进行中',
|
||||
waiting: '等待中',
|
||||
option: '选项',
|
||||
apple: '苹果',
|
||||
banana: '香蕉',
|
||||
orange: '橘子',
|
||||
watermelon: '西瓜',
|
||||
primary: '主要按钮',
|
||||
danger: '危险按钮',
|
||||
default: '默认按钮',
|
||||
dashed: '虚线按钮',
|
||||
icon: '图标按钮',
|
||||
hello: '你好,Ant Design!',
|
||||
release: 'Ant Design 6.0 正式发布!',
|
||||
},
|
||||
en: {
|
||||
range: 'Set Range',
|
||||
text: 'Ant Design use CSS-in-JS technology to provide dynamic & mix theme ability. And which use component level CSS-in-JS solution get your application a better performance.',
|
||||
infoText: 'Info Text',
|
||||
dropdown: 'Dropdown',
|
||||
finished: 'Finished',
|
||||
inProgress: 'In Progress',
|
||||
waiting: 'Waiting',
|
||||
option: 'Option',
|
||||
apple: 'Apple',
|
||||
banana: 'Banana',
|
||||
orange: 'Orange',
|
||||
watermelon: 'Watermelon',
|
||||
primary: 'Primary',
|
||||
danger: 'Danger',
|
||||
default: 'Default',
|
||||
dashed: 'Dashed',
|
||||
icon: 'Icon',
|
||||
hello: 'Hello, Ant Design!',
|
||||
release: 'Ant Design 6.0 is released!',
|
||||
},
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => {
|
||||
return {
|
||||
container: css({
|
||||
backgroundColor: `color-mix(in srgb, ${cssVar.colorBgContainer} 70%, transparent)`,
|
||||
backdropFilter: 'blur(12px)',
|
||||
}),
|
||||
flexAuto: css({ flex: 'auto' }),
|
||||
};
|
||||
});
|
||||
interface ComponentsBlockProps {
|
||||
config?: ConfigProviderProps;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
|
||||
const [locale] = useLocale(locales);
|
||||
const { styles } = useStyle();
|
||||
const { config, style, className, containerClassName } = props;
|
||||
|
||||
return (
|
||||
<ConfigProvider {...config}>
|
||||
<Card className={clsx(containerClassName, styles.container)}>
|
||||
<App>
|
||||
<Flex vertical gap="middle" style={style} className={className}>
|
||||
<ModalPanel title="Ant Design" width="100%">
|
||||
{locale.text}
|
||||
</ModalPanel>
|
||||
<Alert title={locale.infoText} type="info" />
|
||||
{/* Line */}
|
||||
<Flex gap="middle">
|
||||
<div style={{ flex: 'none' }}>
|
||||
<Space.Compact>
|
||||
<Button>{locale.dropdown}</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: Array.from({ length: 5 }).map((_, index) => ({
|
||||
key: `opt${index}`,
|
||||
label: `${locale.option} ${index}`,
|
||||
})),
|
||||
}}
|
||||
>
|
||||
<Button icon={<DownOutlined />} />
|
||||
</Dropdown>
|
||||
</Space.Compact>
|
||||
</div>
|
||||
|
||||
<ColorPicker style={{ flex: 'none' }} />
|
||||
|
||||
<Select
|
||||
style={{ flex: 'auto' }}
|
||||
mode="multiple"
|
||||
maxTagCount="responsive"
|
||||
defaultValue={[{ value: 'apple' }, { value: 'banana' }]}
|
||||
options={[
|
||||
{ value: 'apple', label: locale.apple },
|
||||
{ value: 'banana', label: locale.banana },
|
||||
{ value: 'orange', label: locale.orange },
|
||||
{ value: 'watermelon', label: locale.watermelon },
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Progress style={{ margin: 0 }} percent={60} />
|
||||
|
||||
<Steps
|
||||
current={1}
|
||||
items={[
|
||||
{ title: locale.finished },
|
||||
{ title: locale.inProgress },
|
||||
{ title: locale.waiting },
|
||||
]}
|
||||
/>
|
||||
{/* Line */}
|
||||
<Slider
|
||||
style={{ marginInline: 20 }}
|
||||
range
|
||||
marks={{
|
||||
0: '0°C',
|
||||
26: '26°C',
|
||||
37: '37°C',
|
||||
100: {
|
||||
style: { color: '#f50' },
|
||||
label: <strong>100°C</strong>,
|
||||
},
|
||||
}}
|
||||
defaultValue={[26, 37]}
|
||||
/>
|
||||
{/* Line */}
|
||||
<Flex gap="middle">
|
||||
<Button type="primary" className={styles.flexAuto}>
|
||||
{locale.primary}
|
||||
</Button>
|
||||
<Button type="primary" className={styles.flexAuto} danger>
|
||||
{locale.danger}
|
||||
</Button>
|
||||
<Button className={styles.flexAuto}>{locale.default}</Button>
|
||||
<Button className={styles.flexAuto} type="dashed">
|
||||
{locale.dashed}
|
||||
</Button>
|
||||
</Flex>
|
||||
{/* Line */}
|
||||
<Flex gap="middle">
|
||||
<Switch
|
||||
defaultChecked
|
||||
checkedChildren={<CheckOutlined />}
|
||||
unCheckedChildren={<CloseOutlined />}
|
||||
style={{ width: 48 }}
|
||||
/>
|
||||
<Checkbox.Group
|
||||
options={[locale.apple, locale.banana, locale.orange]}
|
||||
defaultValue={[locale.apple]}
|
||||
/>
|
||||
<Radio.Group defaultValue={locale.apple} options={[locale.apple, locale.banana]} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</App>
|
||||
</Card>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentsBlock;
|
||||
180
.dumi/pages/index/components/ThemePreview/index.tsx
Normal file
180
.dumi/pages/index/components/ThemePreview/index.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import * as React from 'react';
|
||||
import { ConfigProvider, Flex, theme } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { DarkContext } from '../../../../hooks/useDark';
|
||||
import useLocale from '../../../../hooks/useLocale';
|
||||
import Group from '../Group';
|
||||
import ComponentsBlock from './ComponentsBlock';
|
||||
import usePreviewThemes from './previewThemes';
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
themeTitle: '定制主题,随心所欲',
|
||||
themeDesc: '开放样式算法与语义化结构,让你与 AI 一起轻松定制主题',
|
||||
},
|
||||
en: {
|
||||
themeTitle: 'Flexible theme customization',
|
||||
themeDesc:
|
||||
'Open style algorithms and semantic structures make it easy for you and AI to customize themes',
|
||||
},
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ css, cssVar }) => ({
|
||||
container: css({
|
||||
width: '100%',
|
||||
color: cssVar.colorText,
|
||||
lineHeight: cssVar.lineHeight,
|
||||
fontSize: cssVar.fontSize,
|
||||
fontFamily: cssVar.fontFamily,
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'center',
|
||||
}),
|
||||
|
||||
// List
|
||||
list: css({
|
||||
flex: 'auto',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStyleType: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: cssVar.paddingMD,
|
||||
}),
|
||||
listItem: css({
|
||||
margin: 0,
|
||||
fontSize: cssVar.fontSizeLG,
|
||||
lineHeight: cssVar.lineHeightLG,
|
||||
paddingBlock: cssVar.padding,
|
||||
paddingInline: cssVar.paddingLG,
|
||||
border: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorBorderSecondary}`,
|
||||
borderRadius: cssVar.borderRadius,
|
||||
borderColor: 'transparent',
|
||||
transition: `all ${cssVar.motionDurationMid} ${cssVar.motionEaseInOut}`,
|
||||
|
||||
'&:hover:not(.active)': {
|
||||
borderColor: cssVar.colorPrimaryBorder,
|
||||
backgroundColor: cssVar.colorPrimaryBg,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
'&:focus-visible': {
|
||||
outline: `2px solid ${cssVar.colorPrimary}`,
|
||||
outlineOffset: 2,
|
||||
},
|
||||
|
||||
'&.active': {
|
||||
borderColor: cssVar.colorPrimary,
|
||||
backgroundColor: cssVar.colorPrimaryBg,
|
||||
color: cssVar.colorPrimary,
|
||||
},
|
||||
|
||||
// ========= Dark =========
|
||||
'&.dark': {
|
||||
color: cssVar.colorTextLightSolid,
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
'&:hover, &.active': {
|
||||
borderColor: cssVar.colorTextLightSolid,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// Components
|
||||
componentsBlockContainer: css({
|
||||
flex: 'auto',
|
||||
display: 'flex',
|
||||
padding: cssVar.paddingXL,
|
||||
justifyContent: 'center',
|
||||
border: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorBorderSecondary}`,
|
||||
borderRadius: cssVar.borderRadius,
|
||||
boxShadow: cssVar.boxShadow,
|
||||
}),
|
||||
componentsBlock: css({
|
||||
flex: 'none',
|
||||
maxWidth: `calc(420px + ${cssVar.paddingXL} * 2)`,
|
||||
}),
|
||||
}));
|
||||
|
||||
export default function ThemePreview() {
|
||||
const [locale] = useLocale(locales);
|
||||
const { styles } = useStyles();
|
||||
const isDark = React.use(DarkContext);
|
||||
|
||||
const previewThemes = usePreviewThemes();
|
||||
|
||||
const [activeName, setActiveName] = React.useState(() => previewThemes[0].name);
|
||||
|
||||
React.useEffect(() => {
|
||||
const defaultThemeName = isDark ? 'dark' : 'light';
|
||||
|
||||
const targetTheme =
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? previewThemes[previewThemes.length - 1].name
|
||||
: previewThemes.find((theme) => theme.key === defaultThemeName)?.name ||
|
||||
previewThemes[0].name;
|
||||
|
||||
setActiveName(targetTheme);
|
||||
}, [isDark]);
|
||||
|
||||
// 收集所有背景图片用于预加载
|
||||
const backgroundPrefetchList = React.useMemo(
|
||||
() => previewThemes.map((theme) => theme.bgImg).filter((img): img is string => !!img),
|
||||
[previewThemes],
|
||||
);
|
||||
|
||||
const handleThemeClick = (name: string) => {
|
||||
setActiveName(name);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent, name: string) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleThemeClick(name);
|
||||
}
|
||||
};
|
||||
|
||||
const activeTheme = previewThemes.find((theme) => theme.name === activeName);
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
|
||||
<Group
|
||||
title={locale.themeTitle}
|
||||
description={locale.themeDesc}
|
||||
background={activeTheme?.bgImg}
|
||||
titleColor={activeTheme?.bgImgDark ? '#fff' : undefined}
|
||||
backgroundPrefetchList={backgroundPrefetchList}
|
||||
>
|
||||
<Flex className={styles.container} gap="large">
|
||||
<div className={styles.list} role="tablist" aria-label="Theme selection">
|
||||
{previewThemes.map((theme) => (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.listItem,
|
||||
activeName === theme.name && 'active',
|
||||
activeTheme?.bgImgDark && 'dark',
|
||||
)}
|
||||
key={theme.name}
|
||||
role="tab"
|
||||
tabIndex={activeName === theme.name ? 0 : -1}
|
||||
aria-selected={activeName === theme.name}
|
||||
onClick={() => handleThemeClick(theme.name)}
|
||||
onKeyDown={(event) => handleKeyDown(event, theme.name)}
|
||||
>
|
||||
{theme.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ComponentsBlock
|
||||
key={activeName}
|
||||
config={activeTheme?.props}
|
||||
className={styles.componentsBlock}
|
||||
containerClassName={styles.componentsBlockContainer}
|
||||
/>
|
||||
</Flex>
|
||||
</Group>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { useMemo } from 'react';
|
||||
import { theme } from 'antd';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type { UseTheme } from '.';
|
||||
|
||||
const useStyles = createStyles(({ css, cssVar }) => {
|
||||
return {
|
||||
boxBorder: css({
|
||||
border: `${cssVar.lineWidth} ${cssVar.lineType} color-mix(in srgb,${cssVar.colorBorder} 80%, #000)`,
|
||||
}),
|
||||
alertRoot: css({
|
||||
color: cssVar.colorInfoText,
|
||||
textShadow: `0 1px 0 rgba(255, 255, 255, 0.8)`,
|
||||
}),
|
||||
|
||||
modalContainer: css({
|
||||
padding: 0,
|
||||
borderRadius: cssVar.borderRadiusLG,
|
||||
}),
|
||||
modalHeader: css({
|
||||
borderBottom: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorSplit}`,
|
||||
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
|
||||
}),
|
||||
modalBody: css({
|
||||
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
|
||||
}),
|
||||
modalFooter: css({
|
||||
borderTop: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorSplit}`,
|
||||
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
|
||||
backgroundColor: cssVar.colorBgContainerDisabled,
|
||||
boxShadow: `inset 0 1px 0 ${cssVar.colorBgContainer}`,
|
||||
}),
|
||||
buttonRoot: css({
|
||||
backgroundImage: `linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.2))`,
|
||||
boxShadow: `inset 0 1px 0 rgba(255, 255, 255, 0.15)`,
|
||||
transition: 'none',
|
||||
borderColor: `rgba(0, 0, 0, 0.3)`,
|
||||
textShadow: `0 -1px 0 rgba(0, 0, 0, 0.2)`,
|
||||
|
||||
'&:hover, &:active': {
|
||||
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.15) 100%)`,
|
||||
},
|
||||
|
||||
'&:active': {
|
||||
boxShadow: `inset 0 1px 3px rgba(0, 0, 0, 0.15)`,
|
||||
},
|
||||
}),
|
||||
buttonColorDefault: css({
|
||||
textShadow: 'none',
|
||||
color: cssVar.colorText,
|
||||
borderBottomColor: 'rgba(0, 0, 0, 0.5)',
|
||||
}),
|
||||
popupBox: css({
|
||||
borderRadius: cssVar.borderRadiusLG,
|
||||
backgroundColor: cssVar.colorBgContainer,
|
||||
|
||||
ul: {
|
||||
paddingInline: 0,
|
||||
},
|
||||
}),
|
||||
dropdownItem: css({
|
||||
borderRadius: 0,
|
||||
transition: 'none',
|
||||
paddingBlock: cssVar.paddingXXS,
|
||||
paddingInline: cssVar.padding,
|
||||
|
||||
'&:hover, &:active, &:focus': {
|
||||
backgroundImage: `linear-gradient(to bottom, ${cssVar.colorPrimaryHover}, ${cssVar.colorPrimary})`,
|
||||
color: cssVar.colorTextLightSolid,
|
||||
},
|
||||
}),
|
||||
selectPopupRoot: css({
|
||||
paddingInline: 0,
|
||||
}),
|
||||
switchRoot: css({
|
||||
boxShadow: `inset 0 1px 3px rgba(0, 0, 0, 0.4)`,
|
||||
}),
|
||||
progressTrack: css({
|
||||
backgroundImage: `linear-gradient(to bottom, ${cssVar.colorPrimaryHover}, ${cssVar.colorPrimary})`,
|
||||
borderRadius: cssVar.borderRadiusSM,
|
||||
}),
|
||||
progressRail: css({
|
||||
borderRadius: cssVar.borderRadiusSM,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const useBootstrapTheme: UseTheme = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return useMemo<ConfigProviderProps>(
|
||||
() => ({
|
||||
theme: {
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
borderRadiusLG: 6,
|
||||
colorInfo: '#3a87ad',
|
||||
},
|
||||
components: {
|
||||
Tooltip: {
|
||||
fontSize: 12,
|
||||
},
|
||||
Checkbox: {
|
||||
colorBorder: '#666',
|
||||
borderRadius: 2,
|
||||
algorithm: true,
|
||||
},
|
||||
Radio: {
|
||||
colorBorder: '#666',
|
||||
borderRadius: 2,
|
||||
algorithm: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wave: {
|
||||
showEffect: () => {},
|
||||
},
|
||||
|
||||
modal: {
|
||||
classNames: {
|
||||
container: clsx(styles.boxBorder, styles.modalContainer),
|
||||
header: styles.modalHeader,
|
||||
body: styles.modalBody,
|
||||
footer: styles.modalFooter,
|
||||
},
|
||||
},
|
||||
button: {
|
||||
classNames: ({ props }) => ({
|
||||
root: clsx(styles.buttonRoot, props.color === 'default' && styles.buttonColorDefault),
|
||||
}),
|
||||
},
|
||||
|
||||
alert: {
|
||||
className: styles.alertRoot,
|
||||
},
|
||||
colorPicker: {
|
||||
classNames: {
|
||||
root: styles.boxBorder,
|
||||
popup: {
|
||||
root: clsx(styles.boxBorder, styles.popupBox),
|
||||
},
|
||||
},
|
||||
arrow: false,
|
||||
},
|
||||
checkbox: {
|
||||
classNames: {},
|
||||
},
|
||||
dropdown: {
|
||||
classNames: {
|
||||
root: clsx(styles.boxBorder, styles.popupBox),
|
||||
item: styles.dropdownItem,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
classNames: {
|
||||
root: styles.boxBorder,
|
||||
popup: {
|
||||
root: clsx(styles.boxBorder, styles.selectPopupRoot),
|
||||
listItem: styles.dropdownItem,
|
||||
},
|
||||
},
|
||||
},
|
||||
switch: {
|
||||
classNames: {
|
||||
root: styles.switchRoot,
|
||||
},
|
||||
},
|
||||
progress: {
|
||||
classNames: {
|
||||
track: styles.progressTrack,
|
||||
rail: styles.progressRail,
|
||||
},
|
||||
styles: {
|
||||
rail: {
|
||||
height: 20,
|
||||
},
|
||||
track: { height: 20 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
};
|
||||
export default useBootstrapTheme;
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useMemo } from 'react';
|
||||
import { theme } from 'antd';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
import type { UseTheme } from '.';
|
||||
|
||||
const useStyles = createStyles(({ css, cssVar }) => {
|
||||
const sharedBorder = {
|
||||
border: `${cssVar.lineWidth} ${cssVar.lineType} ${cssVar.colorBorder}`,
|
||||
};
|
||||
|
||||
return {
|
||||
sharedBorder,
|
||||
progressTrack: css({
|
||||
...sharedBorder,
|
||||
marginInlineStart: `calc(-1 * ${cssVar.lineWidth})`,
|
||||
marginBlockStart: `calc(-1 * ${cssVar.lineWidth})`,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const useCartoonTheme: UseTheme = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return useMemo<ConfigProviderProps>(
|
||||
() => ({
|
||||
theme: {
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
token: {
|
||||
colorText: '#51463B',
|
||||
colorPrimary: '#225555',
|
||||
colorError: '#DA8787',
|
||||
colorInfo: '#9CD3D3',
|
||||
colorInfoBorder: '#225555',
|
||||
colorBorder: '#225555',
|
||||
colorBorderSecondary: '#225555',
|
||||
lineWidth: 2,
|
||||
lineWidthBold: 2,
|
||||
borderRadius: 18,
|
||||
borderRadiusLG: 18,
|
||||
borderRadiusSM: 18,
|
||||
controlHeightSM: 28,
|
||||
controlHeight: 36,
|
||||
colorBgBase: '#FAFAEE',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
primaryShadow: 'none',
|
||||
dangerShadow: 'none',
|
||||
defaultShadow: 'none',
|
||||
},
|
||||
Modal: {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
Card: {
|
||||
colorBgContainer: '#BBAA99',
|
||||
},
|
||||
Tooltip: {
|
||||
borderRadius: 6,
|
||||
colorBorder: '#225555',
|
||||
algorithm: true,
|
||||
},
|
||||
Select: {
|
||||
optionSelectedBg: '#CBC4AF',
|
||||
},
|
||||
},
|
||||
},
|
||||
// app: {
|
||||
// className: styles.app,
|
||||
// },
|
||||
modal: {
|
||||
classNames: {
|
||||
container: styles.sharedBorder,
|
||||
},
|
||||
},
|
||||
colorPicker: {
|
||||
arrow: false,
|
||||
},
|
||||
popover: {
|
||||
classNames: {
|
||||
container: styles.sharedBorder,
|
||||
},
|
||||
},
|
||||
progress: {
|
||||
classNames: {
|
||||
rail: styles.sharedBorder,
|
||||
track: styles.progressTrack,
|
||||
},
|
||||
styles: {
|
||||
rail: {
|
||||
height: 16,
|
||||
},
|
||||
track: {
|
||||
height: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
export default useCartoonTheme;
|
||||
@@ -0,0 +1,157 @@
|
||||
import { useMemo } from 'react';
|
||||
import { theme } from 'antd';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type { UseTheme } from '.';
|
||||
|
||||
const useStyles = createStyles(({ css, cssVar }) => {
|
||||
const lightBorder = {
|
||||
border: `${cssVar.lineWidth} solid ${cssVar.colorPrimary}`,
|
||||
boxShadow: `0 0 3px ${cssVar.colorPrimary}, inset 0 0 10px ${cssVar.colorPrimary}`,
|
||||
};
|
||||
|
||||
return {
|
||||
lightBorder,
|
||||
app: css({
|
||||
textShadow: `0 0 5px color-mix(in srgb, currentColor 50%, transparent)`,
|
||||
}),
|
||||
modalContainer: css({
|
||||
...lightBorder,
|
||||
padding: 0,
|
||||
}),
|
||||
modalHeader: css({
|
||||
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
|
||||
margin: 0,
|
||||
position: 'relative',
|
||||
|
||||
'&:after': {
|
||||
...lightBorder,
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
border: 0,
|
||||
height: cssVar.lineWidth,
|
||||
background: cssVar.colorPrimary,
|
||||
},
|
||||
}),
|
||||
modalBody: css({
|
||||
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
|
||||
}),
|
||||
modalFooter: css({
|
||||
padding: `${cssVar.padding} ${cssVar.paddingLG}`,
|
||||
}),
|
||||
|
||||
buttonRoot: css({
|
||||
...lightBorder,
|
||||
border: undefined,
|
||||
borderWidth: cssVar.lineWidth,
|
||||
borderColor: cssVar.colorPrimary,
|
||||
}),
|
||||
buttonRootSolid: css({
|
||||
color: cssVar.colorBgContainer,
|
||||
border: 'none',
|
||||
fontWeight: 'bolder',
|
||||
}),
|
||||
buttonRootSolidDanger: css({
|
||||
boxShadow: `0 0 5px ${cssVar.colorError}`,
|
||||
}),
|
||||
|
||||
colorPickerBody: css({
|
||||
pointerEvents: 'none',
|
||||
}),
|
||||
tooltipRoot: css({
|
||||
padding: cssVar.padding,
|
||||
}),
|
||||
tooltipContainer: css({
|
||||
...lightBorder,
|
||||
color: cssVar.colorPrimary,
|
||||
}),
|
||||
progressTrack: css({
|
||||
backgroundColor: cssVar.colorPrimary,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const useGeekTheme: UseTheme = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return useMemo<ConfigProviderProps>(
|
||||
() => ({
|
||||
theme: {
|
||||
algorithm: theme.darkAlgorithm,
|
||||
token: {
|
||||
borderRadius: 0,
|
||||
lineWidth: 2,
|
||||
colorPrimary: '#39ff14',
|
||||
colorText: '#39ff14',
|
||||
controlHeightSM: 26,
|
||||
controlHeight: 34,
|
||||
},
|
||||
},
|
||||
app: {
|
||||
className: styles.app,
|
||||
},
|
||||
modal: {
|
||||
classNames: {
|
||||
container: styles.modalContainer,
|
||||
header: styles.modalHeader,
|
||||
body: styles.modalBody,
|
||||
footer: styles.modalFooter,
|
||||
},
|
||||
},
|
||||
button: {
|
||||
classNames: ({ props }) => ({
|
||||
root: clsx(
|
||||
styles.buttonRoot,
|
||||
props.variant === 'solid' && styles.buttonRootSolid,
|
||||
props.variant === 'solid' && props.danger && styles.buttonRootSolidDanger,
|
||||
),
|
||||
}),
|
||||
},
|
||||
|
||||
alert: {
|
||||
className: styles.lightBorder,
|
||||
},
|
||||
colorPicker: {
|
||||
classNames: {
|
||||
root: styles.lightBorder,
|
||||
body: styles.colorPickerBody,
|
||||
},
|
||||
arrow: false,
|
||||
},
|
||||
select: {
|
||||
classNames: {
|
||||
root: styles.lightBorder,
|
||||
},
|
||||
},
|
||||
input: {
|
||||
classNames: {
|
||||
root: styles.lightBorder,
|
||||
},
|
||||
},
|
||||
inputNumber: {
|
||||
classNames: {
|
||||
root: styles.lightBorder,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
arrow: false,
|
||||
classNames: {
|
||||
root: styles.tooltipRoot,
|
||||
container: styles.tooltipContainer,
|
||||
},
|
||||
},
|
||||
progress: {
|
||||
classNames: {
|
||||
track: styles.progressTrack,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
};
|
||||
export default useGeekTheme;
|
||||
@@ -0,0 +1,157 @@
|
||||
import { useMemo } from 'react';
|
||||
import { theme } from 'antd';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type { UseTheme } from '.';
|
||||
|
||||
const useStyles = createStyles(({ css, cssVar }) => {
|
||||
const glassBorder = {
|
||||
border: `${cssVar.lineWidth} solid rgba(255,255,255,0.3)`,
|
||||
boxShadow: [
|
||||
`${cssVar.boxShadowSecondary}`,
|
||||
`inset 0 0 5px 2px rgba(255, 255, 255, 0.3)`,
|
||||
`inset 0 5px 2px rgba(255, 255, 255, 0.2)`,
|
||||
].join(','),
|
||||
};
|
||||
|
||||
const glassBox = {
|
||||
...glassBorder,
|
||||
background: `color-mix(in srgb, ${cssVar.colorBgContainer} 15%, transparent)`,
|
||||
backdropFilter: 'blur(12px)',
|
||||
};
|
||||
|
||||
return {
|
||||
glassBorder,
|
||||
glassBox,
|
||||
notBackdropFilter: css({
|
||||
backdropFilter: 'none',
|
||||
}),
|
||||
app: css({
|
||||
textShadow: '0 1px rgba(0,0,0,0.1)',
|
||||
}),
|
||||
cardRoot: css({
|
||||
...glassBox,
|
||||
backgroundColor: `color-mix(in srgb, ${cssVar.colorBgContainer} 40%, transparent)`,
|
||||
}),
|
||||
modalContainer: css({
|
||||
...glassBox,
|
||||
backdropFilter: 'none',
|
||||
}),
|
||||
buttonRoot: css({
|
||||
...glassBorder,
|
||||
}),
|
||||
buttonRootDefaultColor: css({
|
||||
background: 'transparent',
|
||||
color: cssVar.colorText,
|
||||
|
||||
'&:hover': {
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
color: `color-mix(in srgb, ${cssVar.colorText} 90%, transparent)`,
|
||||
},
|
||||
|
||||
'&:active': {
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
color: `color-mix(in srgb, ${cssVar.colorText} 80%, transparent)`,
|
||||
},
|
||||
}),
|
||||
|
||||
dropdownRoot: css({
|
||||
...glassBox,
|
||||
borderRadius: cssVar.borderRadiusLG,
|
||||
|
||||
ul: {
|
||||
background: 'transparent',
|
||||
},
|
||||
}),
|
||||
switchRoot: css({ ...glassBorder, border: 'none' }),
|
||||
};
|
||||
});
|
||||
|
||||
const useGlassTheme: UseTheme = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return useMemo<ConfigProviderProps>(
|
||||
() => ({
|
||||
theme: {
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
token: {
|
||||
borderRadius: 12,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 12,
|
||||
borderRadiusXS: 12,
|
||||
motionDurationSlow: '0.2s',
|
||||
motionDurationMid: '0.1s',
|
||||
motionDurationFast: '0.05s',
|
||||
},
|
||||
},
|
||||
app: {
|
||||
className: styles.app,
|
||||
},
|
||||
card: {
|
||||
classNames: {
|
||||
root: styles.cardRoot,
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
classNames: {
|
||||
container: styles.modalContainer,
|
||||
},
|
||||
},
|
||||
button: {
|
||||
classNames: ({ props }) => ({
|
||||
root: clsx(
|
||||
styles.buttonRoot,
|
||||
(props.variant !== 'solid' || props.color === 'default' || props.type === 'default') &&
|
||||
styles.buttonRootDefaultColor,
|
||||
),
|
||||
}),
|
||||
},
|
||||
alert: {
|
||||
className: clsx(styles.glassBox, styles.notBackdropFilter),
|
||||
},
|
||||
colorPicker: {
|
||||
arrow: false,
|
||||
},
|
||||
dropdown: {
|
||||
classNames: {
|
||||
root: styles.dropdownRoot,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
classNames: {
|
||||
root: clsx(styles.glassBox, styles.notBackdropFilter),
|
||||
popup: {
|
||||
root: styles.glassBox,
|
||||
},
|
||||
},
|
||||
},
|
||||
popover: {
|
||||
classNames: {
|
||||
container: styles.glassBox,
|
||||
},
|
||||
},
|
||||
switch: {
|
||||
classNames: {
|
||||
root: styles.switchRoot,
|
||||
},
|
||||
},
|
||||
progress: {
|
||||
classNames: {
|
||||
track: styles.glassBorder,
|
||||
},
|
||||
styles: {
|
||||
track: {
|
||||
height: 12,
|
||||
},
|
||||
rail: {
|
||||
height: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
};
|
||||
export default useGlassTheme;
|
||||
@@ -0,0 +1,184 @@
|
||||
import { useMemo } from 'react';
|
||||
import { theme } from 'antd';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
import type { UseTheme } from '.';
|
||||
|
||||
const useStyles = createStyles(({ css, cssVar }) => {
|
||||
const illustrationBorder = {
|
||||
border: `${cssVar.lineWidth} solid ${cssVar.colorBorder}`,
|
||||
};
|
||||
|
||||
const illustrationBox = {
|
||||
...illustrationBorder,
|
||||
boxShadow: `4px 4px 0 ${cssVar.colorBorder}`,
|
||||
};
|
||||
|
||||
return {
|
||||
illustrationBorder,
|
||||
illustrationBox,
|
||||
buttonRoot: css({
|
||||
...illustrationBox,
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
}),
|
||||
modalContainer: css({
|
||||
...illustrationBox,
|
||||
}),
|
||||
tooltipRoot: css({
|
||||
padding: cssVar.padding,
|
||||
}),
|
||||
popupBox: css({
|
||||
...illustrationBox,
|
||||
borderRadius: cssVar.borderRadiusLG,
|
||||
backgroundColor: cssVar.colorBgContainer,
|
||||
}),
|
||||
progressRail: css({
|
||||
border: `${cssVar.lineWidth} solid ${cssVar.colorBorder}`,
|
||||
boxShadow: `2px 2px 0 ${cssVar.colorBorder}`,
|
||||
}),
|
||||
progressTrack: css({
|
||||
border: 'none',
|
||||
}),
|
||||
inputNumberActions: css({
|
||||
width: 12,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const useIllustrationTheme: UseTheme = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return useMemo<ConfigProviderProps>(
|
||||
() => ({
|
||||
theme: {
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
token: {
|
||||
colorText: '#2C2C2C',
|
||||
colorPrimary: '#52C41A',
|
||||
colorSuccess: '#51CF66',
|
||||
colorWarning: '#FFD93D',
|
||||
colorError: '#FA5252',
|
||||
colorInfo: '#4DABF7',
|
||||
colorBorder: '#2C2C2C',
|
||||
colorBorderSecondary: '#2C2C2C',
|
||||
lineWidth: 3,
|
||||
lineWidthBold: 3,
|
||||
borderRadius: 12,
|
||||
borderRadiusLG: 16,
|
||||
borderRadiusSM: 8,
|
||||
controlHeight: 40,
|
||||
controlHeightSM: 34,
|
||||
controlHeightLG: 48,
|
||||
fontSize: 15,
|
||||
fontWeightStrong: 600,
|
||||
colorBgBase: '#FFF9F0',
|
||||
colorBgContainer: '#FFFFFF',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
primaryShadow: 'none',
|
||||
dangerShadow: 'none',
|
||||
defaultShadow: 'none',
|
||||
fontWeight: 600,
|
||||
},
|
||||
Modal: {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
Card: {
|
||||
boxShadow: '4px 4px 0 #2C2C2C',
|
||||
colorBgContainer: '#FFF0F6',
|
||||
},
|
||||
Tooltip: {
|
||||
colorBorder: '#2C2C2C',
|
||||
colorBgSpotlight: 'rgba(100, 100, 100, 0.95)',
|
||||
borderRadius: 8,
|
||||
},
|
||||
Select: {
|
||||
optionSelectedBg: 'transparent',
|
||||
},
|
||||
Slider: {
|
||||
dotBorderColor: '#237804',
|
||||
dotActiveBorderColor: '#237804',
|
||||
colorPrimaryBorder: '#237804',
|
||||
colorPrimaryBorderHover: '#237804',
|
||||
},
|
||||
},
|
||||
},
|
||||
button: {
|
||||
classNames: {
|
||||
root: styles.buttonRoot,
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
classNames: {
|
||||
container: styles.modalContainer,
|
||||
},
|
||||
},
|
||||
alert: {
|
||||
className: styles.illustrationBorder,
|
||||
},
|
||||
colorPicker: {
|
||||
arrow: false,
|
||||
classNames: {
|
||||
root: styles.illustrationBox,
|
||||
},
|
||||
},
|
||||
popover: {
|
||||
classNames: {
|
||||
container: styles.illustrationBox,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
arrow: false,
|
||||
classNames: {
|
||||
root: styles.tooltipRoot,
|
||||
container: styles.illustrationBox,
|
||||
},
|
||||
},
|
||||
dropdown: {
|
||||
classNames: {
|
||||
root: styles.popupBox,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
classNames: {
|
||||
root: styles.illustrationBox,
|
||||
popup: {
|
||||
root: styles.popupBox,
|
||||
},
|
||||
},
|
||||
},
|
||||
input: {
|
||||
classNames: {
|
||||
root: styles.illustrationBox,
|
||||
},
|
||||
},
|
||||
inputNumber: {
|
||||
classNames: {
|
||||
root: styles.illustrationBox,
|
||||
actions: styles.inputNumberActions,
|
||||
},
|
||||
},
|
||||
progress: {
|
||||
classNames: {
|
||||
rail: styles.progressRail,
|
||||
track: styles.progressTrack,
|
||||
},
|
||||
styles: {
|
||||
rail: {
|
||||
height: 16,
|
||||
},
|
||||
track: {
|
||||
height: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
export default useIllustrationTheme;
|
||||
132
.dumi/pages/index/components/ThemePreview/previewThemes/index.ts
Normal file
132
.dumi/pages/index/components/ThemePreview/previewThemes/index.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
import { theme } from 'antd';
|
||||
|
||||
import useLocale from '../../../../../hooks/useLocale';
|
||||
import useBootstrapTheme from './bootstrapTheme';
|
||||
import useCartoonTheme from './cartoonTheme';
|
||||
import useGeekTheme from './geekTheme';
|
||||
import useGlassTheme from './glassTheme';
|
||||
import useIllustrationTheme from './illustrationTheme';
|
||||
import useMuiTheme from './muiTheme';
|
||||
import useShadcnTheme from './shadcnTheme';
|
||||
|
||||
type PreviewThemeConfig = {
|
||||
name: string;
|
||||
key?: string;
|
||||
props?: ConfigProviderProps;
|
||||
bgImg?: string;
|
||||
bgImgDark?: true;
|
||||
};
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
default: '默认风格',
|
||||
dark: '暗黑风格',
|
||||
geek: '极客风格',
|
||||
glass: '玻璃风格',
|
||||
mui: '类 MUI 风格',
|
||||
shadcn: '类 shadcn 风格',
|
||||
bootstrap: '类 Bootstrap 拟物化风格',
|
||||
cartoon: '卡通风格',
|
||||
illustration: '插画风格',
|
||||
},
|
||||
en: {
|
||||
default: 'Default Style',
|
||||
dark: 'Dark Style',
|
||||
geek: 'Geek Style',
|
||||
glass: 'Glass Style',
|
||||
mui: 'MUI-like Style',
|
||||
shadcn: 'shadcn-like Style',
|
||||
bootstrap: 'Bootstrap Skeuomorphism',
|
||||
cartoon: 'Cartoon Style',
|
||||
illustration: 'Illustration Style',
|
||||
},
|
||||
};
|
||||
|
||||
export type UseTheme = () => ConfigProviderProps;
|
||||
|
||||
export default function usePreviewThemes() {
|
||||
const [locale] = useLocale(locales);
|
||||
|
||||
const cartoonTheme = useCartoonTheme();
|
||||
const illustrationTheme = useIllustrationTheme();
|
||||
const geekTheme = useGeekTheme();
|
||||
const glassTheme = useGlassTheme();
|
||||
const muiTheme = useMuiTheme();
|
||||
const shadcnTheme = useShadcnTheme();
|
||||
const bootstrapTheme = useBootstrapTheme();
|
||||
|
||||
return React.useMemo<PreviewThemeConfig[]>(() => {
|
||||
return [
|
||||
{
|
||||
name: locale.default,
|
||||
key: 'light',
|
||||
bgImg:
|
||||
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*T8IlRaNez08AAAAARwAAAAgAegCCAQ/original',
|
||||
props: {
|
||||
theme: {
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: locale.dark,
|
||||
key: 'dark',
|
||||
dark: true,
|
||||
bgImg:
|
||||
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*ETkNSJ-oUGwAAAAAQ_AAAAgAegCCAQ/original',
|
||||
bgImgDark: true,
|
||||
props: {
|
||||
theme: {
|
||||
algorithm: theme.darkAlgorithm,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: locale.mui,
|
||||
bgImg:
|
||||
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*IFkZRpIKEEkAAAAAQzAAAAgAegCCAQ/original',
|
||||
props: muiTheme,
|
||||
},
|
||||
{
|
||||
name: locale.shadcn,
|
||||
bgImg:
|
||||
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*56tPQbwgFyEAAAAARuAAAAgAegCCAQ/original',
|
||||
props: shadcnTheme,
|
||||
},
|
||||
{
|
||||
name: locale.cartoon,
|
||||
bgImg:
|
||||
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*tgpBT7vYIUsAAAAAQ-AAAAgAegCCAQ/original',
|
||||
props: cartoonTheme,
|
||||
},
|
||||
{
|
||||
name: locale.illustration,
|
||||
bgImg:
|
||||
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*HuVGQKqOER0AAAAARsAAAAgAegCCAQ/original',
|
||||
props: illustrationTheme,
|
||||
},
|
||||
{
|
||||
name: locale.bootstrap,
|
||||
bgImg:
|
||||
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*ZrLfQIO34x4AAAAAS4AAAAgAegCCAQ/original',
|
||||
props: bootstrapTheme,
|
||||
},
|
||||
{
|
||||
name: locale.glass,
|
||||
bgImg:
|
||||
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*PbKXQLie7OAAAAAARTAAAAgAegCCAQ/original',
|
||||
bgImgDark: true,
|
||||
props: glassTheme,
|
||||
},
|
||||
{
|
||||
name: locale.geek,
|
||||
bgImg:
|
||||
'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*fzA2T4ms154AAAAARtAAAAgAegCCAQ/original',
|
||||
bgImgDark: true,
|
||||
props: geekTheme,
|
||||
},
|
||||
];
|
||||
}, [locale]);
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { useMemo } from 'react';
|
||||
import raf from '@rc-component/util/lib/raf';
|
||||
import { theme } from 'antd';
|
||||
import type { ConfigProviderProps, GetProp } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type { UseTheme } from '.';
|
||||
|
||||
type WaveConfig = GetProp<ConfigProviderProps, 'wave'>;
|
||||
|
||||
// Prepare effect holder
|
||||
const createHolder = (node: HTMLElement) => {
|
||||
const { borderWidth } = getComputedStyle(node);
|
||||
const borderWidthNum = Number.parseInt(borderWidth, 10);
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.style.position = 'absolute';
|
||||
div.style.inset = `-${borderWidthNum}px`;
|
||||
div.style.borderRadius = 'inherit';
|
||||
div.style.background = 'transparent';
|
||||
div.style.zIndex = '999';
|
||||
div.style.pointerEvents = 'none';
|
||||
div.style.overflow = 'hidden';
|
||||
node.appendChild(div);
|
||||
|
||||
return div;
|
||||
};
|
||||
|
||||
const createDot = (holder: HTMLElement, color: string, left: number, top: number, size = 0) => {
|
||||
const dot = document.createElement('div');
|
||||
dot.style.position = 'absolute';
|
||||
dot.style.left = `${left}px`;
|
||||
dot.style.top = `${top}px`;
|
||||
dot.style.width = `${size}px`;
|
||||
dot.style.height = `${size}px`;
|
||||
dot.style.borderRadius = '50%';
|
||||
dot.style.background = color;
|
||||
dot.style.transform = 'translate3d(-50%, -50%, 0)';
|
||||
dot.style.transition = 'all 1s ease-out';
|
||||
holder.appendChild(dot);
|
||||
return dot;
|
||||
};
|
||||
|
||||
// Inset Effect
|
||||
const showInsetEffect: WaveConfig['showEffect'] = (node, { event, component }) => {
|
||||
if (component !== 'Button') {
|
||||
return;
|
||||
}
|
||||
|
||||
const holder = createHolder(node);
|
||||
|
||||
const rect = holder.getBoundingClientRect();
|
||||
|
||||
const left = event.clientX - rect.left;
|
||||
const top = event.clientY - rect.top;
|
||||
|
||||
const dot = createDot(holder, 'rgba(255, 255, 255, 0.65)', left, top);
|
||||
|
||||
// Motion
|
||||
raf(() => {
|
||||
dot.ontransitionend = () => {
|
||||
holder.remove();
|
||||
};
|
||||
|
||||
dot.style.width = '200px';
|
||||
dot.style.height = '200px';
|
||||
dot.style.opacity = '0';
|
||||
});
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ css }) => {
|
||||
return {
|
||||
buttonPrimary: css({
|
||||
backgroundColor: '#1976d2',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.02857em',
|
||||
boxShadow:
|
||||
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
|
||||
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}),
|
||||
buttonDefault: css({
|
||||
backgroundColor: '#ffffff',
|
||||
color: 'rgba(0, 0, 0, 0.87)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.23)',
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.02857em',
|
||||
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}),
|
||||
buttonDanger: css({
|
||||
backgroundColor: '#d32f2f',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.02857em',
|
||||
boxShadow:
|
||||
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
|
||||
}),
|
||||
inputRoot: css({
|
||||
borderColor: 'rgba(0, 0, 0, 0.23)',
|
||||
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}),
|
||||
inputElement: css({
|
||||
color: 'rgba(0, 0, 0, 0.87)',
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
}),
|
||||
inputError: css({
|
||||
borderColor: '#d32f2f',
|
||||
}),
|
||||
selectRoot: css({
|
||||
borderColor: 'rgba(0, 0, 0, 0.23)',
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
}),
|
||||
selectPopup: css({
|
||||
borderRadius: '4px',
|
||||
boxShadow:
|
||||
'0px 5px 5px -3px rgba(0,0,0,0.2), 0px 8px 10px 1px rgba(0,0,0,0.14), 0px 3px 14px 2px rgba(0,0,0,0.12)',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const useMuiTheme: UseTheme = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return useMemo<ConfigProviderProps>(
|
||||
() => ({
|
||||
theme: {
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#1976d2',
|
||||
colorSuccess: '#2e7d32',
|
||||
colorWarning: '#ed6c02',
|
||||
colorError: '#d32f2f',
|
||||
colorInfo: '#0288d1',
|
||||
colorTextBase: '#212121',
|
||||
colorBgBase: '#fafafa',
|
||||
colorPrimaryBg: '#e3f2fd',
|
||||
colorPrimaryBgHover: '#bbdefb',
|
||||
colorPrimaryBorder: '#90caf9',
|
||||
colorPrimaryBorderHover: '#64b5f6',
|
||||
colorPrimaryHover: '#42a5f5',
|
||||
colorPrimaryActive: '#1565c0',
|
||||
colorPrimaryText: '#1976d2',
|
||||
colorPrimaryTextHover: '#42a5f5',
|
||||
colorPrimaryTextActive: '#1565c0',
|
||||
colorSuccessBg: '#e8f5e9',
|
||||
colorSuccessBgHover: '#c8e6c9',
|
||||
colorSuccessBorder: '#a5d6a7',
|
||||
colorSuccessBorderHover: '#81c784',
|
||||
colorSuccessHover: '#4caf50',
|
||||
colorSuccessActive: '#1b5e20',
|
||||
colorSuccessText: '#2e7d32',
|
||||
colorSuccessTextHover: '#4caf50',
|
||||
colorSuccessTextActive: '#1b5e20',
|
||||
colorWarningBg: '#fff3e0',
|
||||
colorWarningBgHover: '#ffe0b2',
|
||||
colorWarningBorder: '#ffcc02',
|
||||
colorWarningBorderHover: '#ffb74d',
|
||||
colorWarningHover: '#ff9800',
|
||||
colorWarningActive: '#e65100',
|
||||
colorWarningText: '#ed6c02',
|
||||
colorWarningTextHover: '#ff9800',
|
||||
colorWarningTextActive: '#e65100',
|
||||
colorErrorBg: '#ffebee',
|
||||
colorErrorBgHover: '#ffcdd2',
|
||||
colorErrorBorder: '#ef9a9a',
|
||||
colorErrorBorderHover: '#e57373',
|
||||
colorErrorHover: '#ef5350',
|
||||
colorErrorActive: '#c62828',
|
||||
colorErrorText: '#d32f2f',
|
||||
colorErrorTextHover: '#ef5350',
|
||||
colorErrorTextActive: '#c62828',
|
||||
colorInfoBg: '#e1f5fe',
|
||||
colorInfoBgHover: '#b3e5fc',
|
||||
colorInfoBorder: '#81d4fa',
|
||||
colorInfoBorderHover: '#4fc3f7',
|
||||
colorInfoHover: '#03a9f4',
|
||||
colorInfoActive: '#01579b',
|
||||
colorInfoText: '#0288d1',
|
||||
colorInfoTextHover: '#03a9f4',
|
||||
colorInfoTextActive: '#01579b',
|
||||
colorText: 'rgba(33, 33, 33, 0.87)',
|
||||
colorTextSecondary: 'rgba(33, 33, 33, 0.6)',
|
||||
colorTextTertiary: 'rgba(33, 33, 33, 0.38)',
|
||||
colorTextQuaternary: 'rgba(33, 33, 33, 0.26)',
|
||||
colorTextDisabled: 'rgba(33, 33, 33, 0.38)',
|
||||
colorBgContainer: '#ffffff',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBgLayout: '#f5f5f5',
|
||||
colorBgSpotlight: 'rgba(33, 33, 33, 0.85)',
|
||||
colorBgMask: 'rgba(33, 33, 33, 0.5)',
|
||||
colorBorder: '#e0e0e0',
|
||||
colorBorderSecondary: '#eeeeee',
|
||||
borderRadius: 4,
|
||||
borderRadiusXS: 1,
|
||||
borderRadiusSM: 2,
|
||||
borderRadiusLG: 6,
|
||||
padding: 16,
|
||||
paddingSM: 8,
|
||||
paddingLG: 24,
|
||||
margin: 16,
|
||||
marginSM: 8,
|
||||
marginLG: 24,
|
||||
boxShadow:
|
||||
'0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12)',
|
||||
boxShadowSecondary:
|
||||
'0px 3px 3px -2px rgba(0,0,0,0.2),0px 3px 4px 0px rgba(0,0,0,0.14),0px 1px 8px 0px rgba(0,0,0,0.12)',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
primaryShadow:
|
||||
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
|
||||
defaultShadow:
|
||||
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
|
||||
dangerShadow:
|
||||
'0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)',
|
||||
fontWeight: 500,
|
||||
defaultBorderColor: 'rgba(0, 0, 0, 0.23)',
|
||||
defaultColor: 'rgba(0, 0, 0, 0.87)',
|
||||
defaultBg: '#ffffff',
|
||||
defaultHoverBg: 'rgba(25, 118, 210, 0.04)',
|
||||
defaultHoverBorderColor: 'rgba(0, 0, 0, 0.23)',
|
||||
paddingInline: 16,
|
||||
paddingBlock: 6,
|
||||
contentFontSize: 14,
|
||||
borderRadius: 4,
|
||||
},
|
||||
Alert: {
|
||||
borderRadiusLG: 4,
|
||||
},
|
||||
Modal: {
|
||||
borderRadiusLG: 4,
|
||||
},
|
||||
Progress: {
|
||||
defaultColor: '#1976d2',
|
||||
remainingColor: 'rgba(25, 118, 210, 0.12)',
|
||||
},
|
||||
Steps: {
|
||||
iconSize: 24,
|
||||
},
|
||||
Checkbox: {
|
||||
borderRadiusSM: 2,
|
||||
},
|
||||
Slider: {
|
||||
trackBg: 'rgba(25, 118, 210, 0.26)',
|
||||
trackHoverBg: 'rgba(25, 118, 210, 0.38)',
|
||||
handleSize: 20,
|
||||
handleSizeHover: 20,
|
||||
railSize: 4,
|
||||
},
|
||||
ColorPicker: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
wave: {
|
||||
showEffect: showInsetEffect,
|
||||
},
|
||||
button: {
|
||||
classNames: ({ props }) => ({
|
||||
root: clsx(
|
||||
props.type === 'primary' && styles.buttonPrimary,
|
||||
props.type === 'default' && styles.buttonDefault,
|
||||
props.danger && styles.buttonDanger,
|
||||
),
|
||||
}),
|
||||
},
|
||||
input: {
|
||||
classNames: ({ props }) => ({
|
||||
root: clsx(styles.inputRoot, props.status === 'error' && styles.inputError),
|
||||
input: styles.inputElement,
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
classNames: {
|
||||
root: styles.selectRoot,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[styles],
|
||||
);
|
||||
};
|
||||
export default useMuiTheme;
|
||||
@@ -0,0 +1,222 @@
|
||||
import { useMemo } from 'react';
|
||||
import { theme } from 'antd';
|
||||
import type { ConfigProviderProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type { UseTheme } from '.';
|
||||
|
||||
const useStyles = createStyles(({ css }) => {
|
||||
return {
|
||||
buttonPrimary: css({
|
||||
backgroundColor: '#18181b',
|
||||
color: '#ffffff',
|
||||
border: '1px solid #18181b',
|
||||
fontWeight: 500,
|
||||
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}),
|
||||
buttonDefault: css({
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#18181b',
|
||||
border: '1px solid #e4e4e7',
|
||||
fontWeight: 500,
|
||||
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}),
|
||||
buttonDanger: css({
|
||||
backgroundColor: '#dc2626',
|
||||
color: '#ffffff',
|
||||
border: '1px solid #dc2626',
|
||||
fontWeight: 500,
|
||||
}),
|
||||
inputRoot: css({
|
||||
borderColor: '#e4e4e7',
|
||||
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}),
|
||||
inputElement: css({
|
||||
color: '#18181b',
|
||||
}),
|
||||
inputError: css({
|
||||
borderColor: '#dc2626',
|
||||
}),
|
||||
selectRoot: css({
|
||||
borderColor: '#e4e4e7',
|
||||
}),
|
||||
selectPopup: css({
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const useShadcnTheme: UseTheme = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return useMemo<ConfigProviderProps>(
|
||||
() => ({
|
||||
theme: {
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#262626',
|
||||
colorSuccess: '#22c55e',
|
||||
colorWarning: '#f97316',
|
||||
colorError: '#ef4444',
|
||||
colorInfo: '#262626',
|
||||
colorTextBase: '#262626',
|
||||
colorBgBase: '#ffffff',
|
||||
colorPrimaryBg: '#f5f5f5',
|
||||
colorPrimaryBgHover: '#e5e5e5',
|
||||
colorPrimaryBorder: '#d4d4d4',
|
||||
colorPrimaryBorderHover: '#a3a3a3',
|
||||
colorPrimaryHover: '#404040',
|
||||
colorPrimaryActive: '#171717',
|
||||
colorPrimaryText: '#262626',
|
||||
colorPrimaryTextHover: '#404040',
|
||||
colorPrimaryTextActive: '#171717',
|
||||
colorSuccessBg: '#f0fdf4',
|
||||
colorSuccessBgHover: '#dcfce7',
|
||||
colorSuccessBorder: '#bbf7d0',
|
||||
colorSuccessBorderHover: '#86efac',
|
||||
colorSuccessHover: '#16a34a',
|
||||
colorSuccessActive: '#15803d',
|
||||
colorSuccessText: '#16a34a',
|
||||
colorSuccessTextHover: '#16a34a',
|
||||
colorSuccessTextActive: '#15803d',
|
||||
colorWarningBg: '#fff7ed',
|
||||
colorWarningBgHover: '#fed7aa',
|
||||
colorWarningBorder: '#fdba74',
|
||||
colorWarningBorderHover: '#fb923c',
|
||||
colorWarningHover: '#ea580c',
|
||||
colorWarningActive: '#c2410c',
|
||||
colorWarningText: '#ea580c',
|
||||
colorWarningTextHover: '#ea580c',
|
||||
colorWarningTextActive: '#c2410c',
|
||||
colorErrorBg: '#fef2f2',
|
||||
colorErrorBgHover: '#fecaca',
|
||||
colorErrorBorder: '#fca5a5',
|
||||
colorErrorBorderHover: '#f87171',
|
||||
colorErrorHover: '#dc2626',
|
||||
colorErrorActive: '#b91c1c',
|
||||
colorErrorText: '#dc2626',
|
||||
colorErrorTextHover: '#dc2626',
|
||||
colorErrorTextActive: '#b91c1c',
|
||||
colorInfoBg: '#f5f5f5',
|
||||
colorInfoBgHover: '#e5e5e5',
|
||||
colorInfoBorder: '#d4d4d4',
|
||||
colorInfoBorderHover: '#a3a3a3',
|
||||
colorInfoHover: '#404040',
|
||||
colorInfoActive: '#171717',
|
||||
colorInfoText: '#262626',
|
||||
colorInfoTextHover: '#404040',
|
||||
colorInfoTextActive: '#171717',
|
||||
colorText: '#262626',
|
||||
colorTextSecondary: '#525252',
|
||||
colorTextTertiary: '#737373',
|
||||
colorTextQuaternary: '#a3a3a3',
|
||||
colorTextDisabled: '#a3a3a3',
|
||||
colorBgContainer: '#ffffff',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBgLayout: '#fafafa',
|
||||
colorBgSpotlight: 'rgba(38, 38, 38, 0.85)',
|
||||
colorBgMask: 'rgba(38, 38, 38, 0.45)',
|
||||
colorBorder: '#e5e5e5',
|
||||
colorBorderSecondary: '#f5f5f5',
|
||||
borderRadius: 10,
|
||||
borderRadiusXS: 2,
|
||||
borderRadiusSM: 6,
|
||||
borderRadiusLG: 14,
|
||||
padding: 16,
|
||||
paddingSM: 12,
|
||||
paddingLG: 24,
|
||||
margin: 16,
|
||||
marginSM: 12,
|
||||
marginLG: 24,
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
|
||||
boxShadowSecondary:
|
||||
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
primaryShadow: 'none',
|
||||
defaultShadow: 'none',
|
||||
dangerShadow: 'none',
|
||||
defaultBorderColor: '#e4e4e7',
|
||||
defaultColor: '#18181b',
|
||||
defaultBg: '#ffffff',
|
||||
defaultHoverBg: '#f4f4f5',
|
||||
defaultHoverBorderColor: '#d4d4d8',
|
||||
defaultHoverColor: '#18181b',
|
||||
defaultActiveBg: '#e4e4e7',
|
||||
defaultActiveBorderColor: '#d4d4d8',
|
||||
borderRadius: 6,
|
||||
},
|
||||
Input: {
|
||||
activeShadow: 'none',
|
||||
hoverBorderColor: '#a1a1aa',
|
||||
activeBorderColor: '#18181b',
|
||||
borderRadius: 6,
|
||||
},
|
||||
Select: {
|
||||
optionSelectedBg: '#f4f4f5',
|
||||
optionActiveBg: '#fafafa',
|
||||
optionSelectedFontWeight: 500,
|
||||
borderRadius: 6,
|
||||
},
|
||||
Alert: {
|
||||
borderRadiusLG: 8,
|
||||
},
|
||||
Modal: {
|
||||
borderRadiusLG: 12,
|
||||
},
|
||||
Progress: {
|
||||
defaultColor: '#18181b',
|
||||
remainingColor: '#f4f4f5',
|
||||
},
|
||||
Steps: {
|
||||
iconSize: 32,
|
||||
},
|
||||
Switch: {
|
||||
trackHeight: 24,
|
||||
trackMinWidth: 44,
|
||||
innerMinMargin: 4,
|
||||
innerMaxMargin: 24,
|
||||
},
|
||||
Checkbox: {
|
||||
borderRadiusSM: 4,
|
||||
},
|
||||
Slider: {
|
||||
trackBg: '#f4f4f5',
|
||||
trackHoverBg: '#e4e4e7',
|
||||
handleSize: 18,
|
||||
handleSizeHover: 20,
|
||||
railSize: 6,
|
||||
},
|
||||
ColorPicker: {
|
||||
borderRadius: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
button: {
|
||||
classNames: ({ props }) => ({
|
||||
root: clsx(
|
||||
props.type === 'primary' && styles.buttonPrimary,
|
||||
props.type === 'default' && styles.buttonDefault,
|
||||
props.danger && styles.buttonDanger,
|
||||
),
|
||||
}),
|
||||
},
|
||||
input: {
|
||||
classNames: ({ props }) => ({
|
||||
root: clsx(styles.inputRoot, props.status === 'error' && styles.inputError),
|
||||
input: styles.inputElement,
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
classNames: {
|
||||
root: styles.selectRoot,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[styles],
|
||||
);
|
||||
};
|
||||
export default useShadcnTheme;
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable compat/compat */
|
||||
import { css } from 'antd-style';
|
||||
import useSWR from 'swr';
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { 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';
|
||||
import ThemePreview from './components/ThemePreview';
|
||||
|
||||
const ComponentsList = React.lazy(() => import('./components/ComponentsList'));
|
||||
const DesignFramework = React.lazy(() => import('./components/DesignFramework'));
|
||||
const Theme = React.lazy(() => import('./components/Theme'));
|
||||
// const Theme = React.lazy(() => import('./components/Theme'));
|
||||
|
||||
const useStyle = createStyles(() => ({
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
image: css`
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
@@ -38,7 +39,6 @@ const locales = {
|
||||
|
||||
const Homepage: React.FC = () => {
|
||||
const [locale] = useLocale(locales);
|
||||
const { styles } = useStyle();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const isDark = React.use(DarkContext);
|
||||
@@ -49,48 +49,96 @@ const Homepage: React.FC = () => {
|
||||
<BannerRecommends />
|
||||
</PreviewBanner>
|
||||
|
||||
<div>
|
||||
{/* 定制主题 */}
|
||||
<ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
|
||||
<Suspense fallback={null}>
|
||||
<Theme />
|
||||
</Suspense>
|
||||
</ConfigProvider>
|
||||
{/* 定制主题 */}
|
||||
{/* <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
|
||||
<Suspense fallback={null}>
|
||||
<Theme />
|
||||
</Suspense>
|
||||
</ConfigProvider> */}
|
||||
<ThemePreview />
|
||||
|
||||
{/* 组件列表 */}
|
||||
<Group
|
||||
background={token.colorBgElevated}
|
||||
collapse
|
||||
title={locale.assetsTitle}
|
||||
description={locale.assetsDesc}
|
||||
id="design"
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<ComponentsList />
|
||||
</Suspense>
|
||||
</Group>
|
||||
{/* 组件列表 */}
|
||||
<Group
|
||||
background={token.colorBgElevated}
|
||||
collapse
|
||||
title={locale.assetsTitle}
|
||||
description={locale.assetsDesc}
|
||||
id="design"
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<ComponentsList />
|
||||
</Suspense>
|
||||
</Group>
|
||||
|
||||
{/* 设计语言 */}
|
||||
<Group
|
||||
title={locale.designTitle}
|
||||
description={locale.designDesc}
|
||||
background={isDark ? '#393F4A' : '#F5F8FF'}
|
||||
decoration={
|
||||
<img
|
||||
draggable={false}
|
||||
className={styles.image}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/ba37a413-28e6-4be4-b1c5-01be1a0ebb1c.svg"
|
||||
alt="bg"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<DesignFramework />
|
||||
</Suspense>
|
||||
</Group>
|
||||
</div>
|
||||
{/* 设计语言 */}
|
||||
<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>
|
||||
</section>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <section>
|
||||
// <PreviewBanner>
|
||||
// <BannerRecommends />
|
||||
// </PreviewBanner>
|
||||
|
||||
// <div>
|
||||
// {/* 定制主题 */}
|
||||
// <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
|
||||
// <Suspense fallback={null}>
|
||||
// <Theme />
|
||||
// </Suspense>
|
||||
// </ConfigProvider>
|
||||
|
||||
// {/* 组件列表 */}
|
||||
// <Group
|
||||
// background={token.colorBgElevated}
|
||||
// collapse
|
||||
// title={locale.assetsTitle}
|
||||
// description={locale.assetsDesc}
|
||||
// id="design"
|
||||
// >
|
||||
// <Suspense fallback={null}>
|
||||
// <ComponentsList />
|
||||
// </Suspense>
|
||||
// </Group>
|
||||
|
||||
// {/* 设计语言 */}
|
||||
// <Group
|
||||
// title={locale.designTitle}
|
||||
// description={locale.designDesc}
|
||||
// background={isDark ? '#393F4A' : '#F5F8FF'}
|
||||
// decoration={
|
||||
// <img
|
||||
// draggable={false}
|
||||
// className={classNames.image}
|
||||
// src="https://gw.alipayobjects.com/zos/bmw-prod/ba37a413-28e6-4be4-b1c5-01be1a0ebb1c.svg"
|
||||
// alt="bg"
|
||||
// />
|
||||
// }
|
||||
// >
|
||||
// <Suspense fallback={null}>
|
||||
// <DesignFramework />
|
||||
// </Suspense>
|
||||
// </Group>
|
||||
// </div>
|
||||
// </section>
|
||||
// );
|
||||
};
|
||||
|
||||
export default Homepage;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { App, Button, ConfigProvider, Skeleton, version } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
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';
|
||||
@@ -9,7 +9,7 @@ import useLocale from '../../hooks/useLocale';
|
||||
|
||||
const ThemeEditor = React.lazy(() => import('antd-token-previewer/lib/ThemeEditor'));
|
||||
|
||||
const useStyle = createStyles(({ css }) => ({
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
editor: css`
|
||||
svg,
|
||||
img {
|
||||
@@ -49,7 +49,6 @@ const ANT_DESIGN_V5_THEME_EDITOR_THEME = `ant-design-v${antdMajor}-theme-editor-
|
||||
const CustomTheme: React.FC = () => {
|
||||
const { message } = App.useApp();
|
||||
const [locale, lang] = useLocale(locales);
|
||||
const { styles } = useStyle();
|
||||
|
||||
const [theme, setTheme] = React.useState<ThemeConfig>(() => {
|
||||
try {
|
||||
@@ -78,7 +77,7 @@ const CustomTheme: React.FC = () => {
|
||||
hideAdvancedSwitcher
|
||||
theme={{ name: 'Custom Theme', key: 'test', config: theme }}
|
||||
style={{ height: 'calc(100vh - 64px)' }}
|
||||
className={styles.editor}
|
||||
className={classNames.editor}
|
||||
onThemeChange={(newTheme) => {
|
||||
setTheme(newTheme.config);
|
||||
}}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { SoundOutlined } from '@ant-design/icons';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => {
|
||||
const styles = createStaticStyles(({ css, cssVar }) => {
|
||||
return {
|
||||
playBtn: css`
|
||||
display: inline-flex;
|
||||
@@ -30,7 +30,6 @@ interface AudioProps {
|
||||
}
|
||||
|
||||
const AudioControl: React.FC<React.PropsWithChildren<AudioProps>> = ({ id, children }) => {
|
||||
const { styles } = useStyle();
|
||||
const onClick: React.MouseEventHandler<HTMLAnchorElement> = () => {
|
||||
const audio = document.querySelector<HTMLAudioElement>(`#${id}`);
|
||||
audio?.play();
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
BugOutlined,
|
||||
CompassOutlined,
|
||||
EditOutlined,
|
||||
FileTextOutlined,
|
||||
GithubOutlined,
|
||||
HistoryOutlined,
|
||||
IssuesCloseOutlined,
|
||||
@@ -32,6 +33,7 @@ const locales = {
|
||||
version: '版本',
|
||||
issueNew: '提交问题',
|
||||
issueOpen: '待解决',
|
||||
copyError: '复制失败',
|
||||
},
|
||||
en: {
|
||||
import: 'Import',
|
||||
@@ -45,6 +47,7 @@ const locales = {
|
||||
version: 'Version',
|
||||
issueNew: 'Issue',
|
||||
issueOpen: 'Open issues',
|
||||
copyError: 'Copy failed',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -128,21 +131,22 @@ const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
|
||||
};
|
||||
|
||||
// ======================== Source ========================
|
||||
const [filledSource, abbrSource] = React.useMemo(() => {
|
||||
const [filledSource, abbrSource, llmsPath] = React.useMemo(() => {
|
||||
if (String(source) === 'true') {
|
||||
const kebabComponent = kebabCase(component);
|
||||
return [
|
||||
`https://github.com/${repo}/blob/master/components/${kebabComponent}`,
|
||||
`components/${kebabComponent}`,
|
||||
`/components/${kebabComponent}${isZhCN ? '-cn' : ''}.md`,
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof source !== 'string') {
|
||||
return [null, null];
|
||||
return [null, null, null];
|
||||
}
|
||||
|
||||
return [source, source];
|
||||
}, [component, repo, source]);
|
||||
return [source, source, null];
|
||||
}, [component, repo, source, isZhCN]);
|
||||
|
||||
return (
|
||||
<Descriptions
|
||||
@@ -210,6 +214,15 @@ const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
|
||||
<span>{locale.design}</span>
|
||||
</Link>
|
||||
)}
|
||||
<Typography.Link
|
||||
className={styles.code}
|
||||
href={llmsPath || ''}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<FileTextOutlined className={styles.icon} />
|
||||
<span>LLMs.md</span>
|
||||
</Typography.Link>
|
||||
<ComponentChangelog>
|
||||
<Typography.Link className={styles.code}>
|
||||
<HistoryOutlined className={styles.icon} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { memo, useMemo, useRef, useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Affix, Card, Col, Divider, Flex, Input, Row, Tag, Typography } from 'antd';
|
||||
import { createStyles, useTheme } from 'antd-style';
|
||||
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';
|
||||
@@ -12,7 +12,7 @@ import SiteContext from '../../slots/SiteContext';
|
||||
import type { Component } from './ProComponentsList';
|
||||
import proComponentsList from './ProComponentsList';
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
componentsOverviewGroupTitle: css`
|
||||
margin-bottom: ${cssVar.marginLG} !important;
|
||||
`,
|
||||
@@ -78,7 +78,6 @@ const reportSearch = debounce<(value: string) => void>((value) => {
|
||||
const { Title } = Typography;
|
||||
|
||||
const Overview: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
const { isDark } = React.use(SiteContext);
|
||||
|
||||
const data = useSidebarData();
|
||||
@@ -227,7 +226,7 @@ const Overview: React.FC = () => {
|
||||
src={
|
||||
isDark && component.coverDark ? component.coverDark : component.cover
|
||||
}
|
||||
alt={component.title}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { App } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { useIntl } from 'dumi';
|
||||
|
||||
import CopyableIcon from './CopyableIcon';
|
||||
import type { CategoriesKeys } from './fields';
|
||||
import type { ThemeType } from './IconSearch';
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
anticonsList: css`
|
||||
margin: ${cssVar.margin} 0;
|
||||
overflow: hidden;
|
||||
@@ -36,7 +36,6 @@ interface CategoryProps {
|
||||
const Category: React.FC<CategoryProps> = (props) => {
|
||||
const { message } = App.useApp();
|
||||
const { icons, title, newIcons, theme } = props;
|
||||
const { styles } = useStyle();
|
||||
const intl = useIntl();
|
||||
const [justCopied, setJustCopied] = React.useState<string | null>(null);
|
||||
const copyId = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import Icon, * as AntdIcons from '@ant-design/icons';
|
||||
import { Affix, Empty, Input, Segmented } from 'antd';
|
||||
import { createStyles, useTheme } from 'antd-style';
|
||||
import { createStaticStyles, useTheme } from 'antd-style';
|
||||
import type { SegmentedOptions } from 'antd/es/segmented';
|
||||
import { useIntl } from 'dumi';
|
||||
import debounce from 'lodash/debounce';
|
||||
@@ -22,7 +22,7 @@ export enum ThemeType {
|
||||
|
||||
const allIcons: { [key: string]: any } = AntdIcons;
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
iconSearchAffix: css`
|
||||
display: flex;
|
||||
transition: all ${cssVar.motionDurationSlow};
|
||||
@@ -39,7 +39,6 @@ const NEW_ICON_NAMES: ReadonlyArray<string> = [];
|
||||
|
||||
const IconSearch: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { styles } = useStyle();
|
||||
const [displayState, setDisplayState] = useState<IconSearchState>({
|
||||
searchKey: '',
|
||||
theme: ThemeType.Outlined,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { Skeleton } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
const IconSearch = React.lazy(() => import('./IconSearch'));
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
searchWrapper: css`
|
||||
display: flex;
|
||||
gap: ${cssVar.padding};
|
||||
@@ -34,8 +34,6 @@ const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
}));
|
||||
|
||||
const IconSearchFallback: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.searchWrapper}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface IconProps {
|
||||
@@ -7,7 +7,7 @@ interface IconProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const useStyle = createStyles(() => ({
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
iconWrap: css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -19,9 +19,8 @@ const useStyle = createStyles(() => ({
|
||||
|
||||
const BunIcon: React.FC<IconProps> = (props) => {
|
||||
const { className, style } = props;
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<span className={clsx(styles.iconWrap, className)} style={style}>
|
||||
<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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface IconProps {
|
||||
@@ -7,7 +7,7 @@ interface IconProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const useStyle = createStyles(() => ({
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
iconWrap: css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -19,9 +19,8 @@ const useStyle = createStyles(() => ({
|
||||
|
||||
const NpmIcon: React.FC<IconProps> = (props) => {
|
||||
const { className, style } = props;
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<span className={clsx(styles.iconWrap, className)} style={style}>
|
||||
<span className={clsx(classNames.iconWrap, className)} style={style}>
|
||||
<svg
|
||||
fill="#E53E3E"
|
||||
focusable="false"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface IconProps {
|
||||
@@ -7,7 +7,7 @@ interface IconProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const useStyle = createStyles(() => ({
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
iconWrap: css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -19,9 +19,8 @@ const useStyle = createStyles(() => ({
|
||||
|
||||
const PnpmIcon: React.FC<IconProps> = (props) => {
|
||||
const { className, style } = props;
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<span className={clsx(styles.iconWrap, className)} style={style}>
|
||||
<span className={clsx(classNames.iconWrap, className)} style={style}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="#F69220"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface IconProps {
|
||||
@@ -7,7 +7,7 @@ interface IconProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const useStyle = createStyles(() => ({
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
iconWrap: css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -19,9 +19,8 @@ const useStyle = createStyles(() => ({
|
||||
|
||||
const YarnIcon: React.FC<IconProps> = (props) => {
|
||||
const { className, style } = props;
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<span className={clsx(styles.iconWrap, className)} style={style}>
|
||||
<span className={clsx(classNames.iconWrap, className)} style={style}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="#2C8EBB"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { Suspense, useRef } from 'react';
|
||||
import { LinkOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import { BugOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import stackblitzSdk from '@stackblitz/sdk';
|
||||
import { Flex, Tooltip } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Button, Dropdown, Flex, Tooltip } from 'antd';
|
||||
import { FormattedMessage, useSiteData } from 'dumi';
|
||||
import LZString from 'lz-string';
|
||||
|
||||
@@ -28,8 +29,6 @@ function compress(string: string): string {
|
||||
}
|
||||
|
||||
interface ActionsProps {
|
||||
showOnlineUrl: boolean;
|
||||
docsOnlineUrl?: string;
|
||||
assetId: string;
|
||||
title?: string;
|
||||
pkgDependencyList: Record<PropertyKey, string>;
|
||||
@@ -39,11 +38,10 @@ interface ActionsProps {
|
||||
onCodeExpand: () => void;
|
||||
entryCode: string;
|
||||
styleCode: string;
|
||||
debugOptions?: MenuProps['items'];
|
||||
}
|
||||
|
||||
const Actions: React.FC<ActionsProps> = ({
|
||||
showOnlineUrl,
|
||||
docsOnlineUrl,
|
||||
assetId,
|
||||
title,
|
||||
jsx,
|
||||
@@ -53,6 +51,7 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
pkgDependencyList,
|
||||
entryCode,
|
||||
styleCode,
|
||||
debugOptions,
|
||||
}) => {
|
||||
const [, lang] = useLocale();
|
||||
const isZhCN = lang === 'cn';
|
||||
@@ -128,8 +127,8 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
editors: '001',
|
||||
css: '',
|
||||
js_external: [
|
||||
'react@19/cjs/react.development.js',
|
||||
'react-dom@19/cjs/react-dom.development.js',
|
||||
'react@18/umd/react.production.min.js',
|
||||
'react-dom@18/umd/react-dom.production.min.js',
|
||||
'dayjs@1/dayjs.min.js',
|
||||
`antd@${pkg.version}/dist/antd-with-locales.min.js`,
|
||||
`@ant-design/icons/dist/index.umd.js`,
|
||||
@@ -213,21 +212,15 @@ createRoot(document.getElementById('container')).render(<Demo />);
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex wrap gap="middle" className="code-box-actions">
|
||||
{/* 在线文档按钮 */}
|
||||
{showOnlineUrl && (
|
||||
<Tooltip title={<FormattedMessage id="app.demo.online" />}>
|
||||
<a
|
||||
className="code-box-code-action"
|
||||
aria-label="open in new tab"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href={docsOnlineUrl || ''}
|
||||
>
|
||||
<LinkOutlined className="code-box-online" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Flex wrap gap="middle" className="code-box-actions" align="center">
|
||||
{
|
||||
// 调试选项
|
||||
debugOptions?.length ? (
|
||||
<Dropdown menu={{ items: debugOptions }} arrow={{ pointAtCenter: true }}>
|
||||
<Button icon={<BugOutlined />} color="purple" variant="filled" size="small" />
|
||||
</Dropdown>
|
||||
) : null
|
||||
}
|
||||
{/* CodeSandbox 按钮 */}
|
||||
<form
|
||||
className="code-box-code-action"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { UpOutlined } from '@ant-design/icons';
|
||||
import { Badge, Tooltip } from 'antd';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Badge, Tag, Tooltip } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
import { FormattedMessage, useLiveDemo } from 'dumi';
|
||||
|
||||
import { FormattedMessage, useLiveDemo, useSiteData } from 'dumi';
|
||||
import { major, minVersion } from 'semver';
|
||||
import type { AntdPreviewerProps } from '.';
|
||||
import useLocation from '../../../hooks/useLocation';
|
||||
import BrowserFrame from '../../common/BrowserFrame';
|
||||
@@ -12,9 +13,11 @@ import ClientOnly from '../../common/ClientOnly';
|
||||
import CodePreview from '../../common/CodePreview';
|
||||
import EditButton from '../../common/EditButton';
|
||||
import SiteContext from '../../slots/SiteContext';
|
||||
import DemoContext from '../../slots/DemoContext';
|
||||
import { isOfficialHost } from '../../utils';
|
||||
import Actions from './Actions';
|
||||
|
||||
const useStyle = createStyles(({ cssVar }) => {
|
||||
const styles = createStaticStyles(({ cssVar, css }) => {
|
||||
return {
|
||||
codeHideBtn: css`
|
||||
position: sticky;
|
||||
@@ -61,8 +64,10 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => {
|
||||
clientOnly,
|
||||
pkgDependencyList,
|
||||
} = props;
|
||||
const { showDebug } = React.use(DemoContext);
|
||||
const { pkg } = useSiteData();
|
||||
|
||||
const location = useLocation();
|
||||
const { styles } = useStyle();
|
||||
|
||||
const entryName = 'index.tsx';
|
||||
const entryCode = asset.dependencies[entryName].value;
|
||||
@@ -79,17 +84,15 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => {
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null);
|
||||
const [codeExpand, setCodeExpand] = useState<boolean>(false);
|
||||
const { isDark } = React.use(SiteContext);
|
||||
|
||||
const { hash, pathname, search } = location;
|
||||
const docsOnlineUrl = `https://ant.design${pathname ?? ''}${search ?? ''}#${asset.id}`;
|
||||
|
||||
const [showOnlineUrl, setShowOnlineUrl] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Record whether it is deployed on the official domain name.
|
||||
* Note that window.location.hostname is not available on the server side due to hydration issues
|
||||
*/
|
||||
const [deployedOnOfficialHost, setDeployedOnOfficialHost] = useState<boolean>(true);
|
||||
useEffect(() => {
|
||||
const regexp = /preview-(\d+)-ant-design/; // matching PR preview addresses
|
||||
setShowOnlineUrl(
|
||||
process.env.NODE_ENV === 'development' || regexp.test(window.location.hostname),
|
||||
);
|
||||
setDeployedOnOfficialHost(isOfficialHost(window.location.hostname));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -102,6 +105,33 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => {
|
||||
setCodeExpand(expand);
|
||||
}, [expand]);
|
||||
|
||||
const generateDocUrl = (domain = 'https://ant.design') =>
|
||||
`${domain}${pathname ?? ''}${search ?? ''}#${asset.id}`;
|
||||
|
||||
// Enable "Go Online Docs" only when deployed on non-official domains
|
||||
const enableDocsOnlineUrl = process.env.NODE_ENV === 'development' || !deployedOnOfficialHost;
|
||||
|
||||
// Previous version demos are only available during the maintenance window
|
||||
const [supportsPreviousVersionDemo, previousVersionDomain, previousVersion] = useMemo(() => {
|
||||
const maintenanceDeadline = new Date('2026/12/31');
|
||||
|
||||
if (new Date() > maintenanceDeadline) {
|
||||
return [false, undefined, -1] as const;
|
||||
}
|
||||
|
||||
const currentMajor = major(pkg.version);
|
||||
const previousMajor = Math.min(currentMajor - 1, 5);
|
||||
|
||||
let enabled = true;
|
||||
// If the demo specifies a version, perform an additional check;
|
||||
if (version) {
|
||||
const minVer = minVersion(version);
|
||||
enabled = minVer?.major ? minVer.major < currentMajor : true;
|
||||
}
|
||||
|
||||
return [enabled, `https://${previousMajor}x.ant.design`, previousMajor];
|
||||
}, [version, pkg.version]);
|
||||
|
||||
const mergedChildren = !iframe && clientOnly ? <ClientOnly>{children}</ClientOnly> : children;
|
||||
const demoUrlWithTheme = useMemo(() => {
|
||||
return `${demoUrl}${isDark ? '?theme=dark' : ''}`;
|
||||
@@ -143,6 +173,47 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => {
|
||||
backgroundColor: background === 'grey' ? backgroundGrey : undefined,
|
||||
};
|
||||
|
||||
const debugOptions: MenuProps['items'] = [
|
||||
{
|
||||
key: 'online',
|
||||
label: (
|
||||
<a
|
||||
aria-label="Go to online documentation"
|
||||
href={generateDocUrl()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<FormattedMessage id="app.demo.online" />
|
||||
</a>
|
||||
),
|
||||
icon: (
|
||||
<Tag variant="filled" color="blue">
|
||||
ant.design
|
||||
</Tag>
|
||||
),
|
||||
enabled: enableDocsOnlineUrl,
|
||||
},
|
||||
{
|
||||
key: 'previousVersion',
|
||||
label: (
|
||||
<a
|
||||
aria-label="Go to previous version documentation"
|
||||
href={generateDocUrl(previousVersionDomain)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<FormattedMessage id="app.demo.previousVersion" values={{ version: previousVersion }} />
|
||||
</a>
|
||||
),
|
||||
icon: (
|
||||
<Tag variant="filled" color="purple">
|
||||
v{previousVersion}
|
||||
</Tag>
|
||||
),
|
||||
enabled: supportsPreviousVersionDemo,
|
||||
},
|
||||
].filter(({ enabled }) => showDebug && enabled);
|
||||
|
||||
const codeBox: React.ReactNode = (
|
||||
<section className={codeBoxClass} id={asset.id}>
|
||||
<section
|
||||
@@ -174,8 +245,7 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => {
|
||||
/>
|
||||
)}
|
||||
<Actions
|
||||
showOnlineUrl={showOnlineUrl}
|
||||
docsOnlineUrl={docsOnlineUrl}
|
||||
debugOptions={debugOptions}
|
||||
entryCode={entryCode}
|
||||
styleCode={style}
|
||||
pkgDependencyList={pkgDependencyList}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Skeleton } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
skeletonWrapper: css`
|
||||
width: 100% !important;
|
||||
height: 250px;
|
||||
@@ -12,7 +12,6 @@ const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
}));
|
||||
|
||||
const DemoFallback = () => {
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<Skeleton.Node
|
||||
active
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FC } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { CheckOutlined, SketchOutlined } from '@ant-design/icons';
|
||||
import { App } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import copy from '../../../../components/_util/copy';
|
||||
import { nodeToGroup } from 'html2sketch';
|
||||
|
||||
@@ -22,12 +22,12 @@ const locales = {
|
||||
},
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
wrapper: css`
|
||||
position: relative;
|
||||
border: 1px solid ${cssVar.colorBorderSecondary};
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
padding: ${cssVar.paddingMD} ${cssVar.paddingLG} ${cssVar.paddingMD * 2};
|
||||
padding: ${cssVar.paddingMD} ${cssVar.paddingLG} calc(${cssVar.paddingMD} * 2);
|
||||
margin-bottom: ${cssVar.marginLG};
|
||||
`,
|
||||
title: css`
|
||||
@@ -67,12 +67,11 @@ const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
`,
|
||||
tip: css`
|
||||
color: ${cssVar.colorTextTertiary};
|
||||
margin-top: ${cssVar.marginMD * 2};
|
||||
margin-top: calc(${cssVar.marginMD} * 2);
|
||||
`,
|
||||
}));
|
||||
|
||||
const DesignPreviewer: FC<AntdPreviewerProps> = ({ children, title, description, tip, asset }) => {
|
||||
const { styles } = useStyle();
|
||||
const demoRef = useRef<HTMLDivElement>(null);
|
||||
const [copied, setCopied] = React.useState<boolean>(false);
|
||||
const { message } = App.useApp();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { BugOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Popover, theme } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import dayjs from 'dayjs';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
@@ -33,7 +33,7 @@ const locales = {
|
||||
},
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
container: css`
|
||||
margin-block: ${cssVar.margin};
|
||||
padding: ${cssVar.padding};
|
||||
@@ -49,8 +49,6 @@ const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const RefinedChangelog: React.FC<React.PropsWithChildren<RefinedChangelogProps>> = (props) => {
|
||||
const { version, date, children } = props;
|
||||
|
||||
const { styles, cx } = useStyle();
|
||||
|
||||
const memoizedValue = React.useMemo<ContextProps>(() => {
|
||||
const realVersion = version || '0.0.0';
|
||||
const bugVersionInfo = matchDeprecated(realVersion);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import type { SandpackSetup } from '@codesandbox/sandpack-react';
|
||||
import { Skeleton } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { useSearchParams } from 'dumi';
|
||||
|
||||
import { version } from '../../../../package.json';
|
||||
@@ -17,7 +17,7 @@ const root = createRoot(document.getElementById("root"));
|
||||
root.render(<App />);
|
||||
`;
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
fallback: css`
|
||||
width: 100%;
|
||||
> * {
|
||||
@@ -32,7 +32,6 @@ const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
}));
|
||||
|
||||
const SandpackFallback: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<div className={styles.fallback}>
|
||||
<Skeleton.Node active style={{ height: 500, width: '100%' }}>
|
||||
@@ -83,6 +82,7 @@ const Sandpack: React.FC<React.PropsWithChildren<SandpackProps>> = (props) => {
|
||||
<OriginSandpack
|
||||
theme={searchParams.getAll('theme').includes('dark') ? 'dark' : undefined}
|
||||
customSetup={setup}
|
||||
template="vite-react-ts"
|
||||
options={options}
|
||||
files={{
|
||||
'index.tsx': indexContent,
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import React from 'react';
|
||||
import { FastColor } from '@ant-design/fast-color';
|
||||
import { Flex, theme } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import tokenMeta from 'antd/es/version/token-meta.json';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => {
|
||||
const styles = createStaticStyles(({ cssVar, css }) => {
|
||||
const height = cssVar.controlHeightLG;
|
||||
const dotSize = height / 5;
|
||||
|
||||
@@ -63,7 +63,6 @@ interface ColorCircleProps {
|
||||
}
|
||||
|
||||
const ColorCircle: React.FC<ColorCircleProps> = ({ color }) => {
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<Flex align="center" gap={4}>
|
||||
<div className={styles.dot} style={{ background: color }} />
|
||||
@@ -79,7 +78,6 @@ export interface TokenCompareProps {
|
||||
const TokenCompare: React.FC<TokenCompareProps> = (props) => {
|
||||
const { tokenNames = '' } = props;
|
||||
const [, lang] = useLocale();
|
||||
const { styles } = useStyle();
|
||||
|
||||
const tokenList = React.useMemo(() => {
|
||||
const list = tokenNames.split('|');
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { PauseCircleFilled, PlayCircleFilled } from '@ant-design/icons';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
const useStyles = createStyles(({ cx, cssVar }) => {
|
||||
const styles = createStaticStyles(({ css, cx, cssVar }) => {
|
||||
const play = css`
|
||||
position: absolute;
|
||||
inset-inline-end: ${cssVar.paddingLG};
|
||||
@@ -45,7 +45,6 @@ const VideoPlayer: React.FC<React.HtmlHTMLAttributes<HTMLVideoElement>> = ({
|
||||
className,
|
||||
...restProps
|
||||
}) => {
|
||||
const { styles } = useStyles();
|
||||
const videoRef = React.useRef<HTMLVideoElement>(null);
|
||||
const [playing, setPlaying] = React.useState(false);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { useRouteMeta } from 'dumi';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
@@ -17,12 +17,12 @@ export interface BehaviorMapProps {
|
||||
data: BehaviorMapItem;
|
||||
}
|
||||
|
||||
const useStyle = createStyles(({ cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
height: fit-content;
|
||||
background-color: #f5f5f5;
|
||||
background-color: ${cssVar.colorBgLayout};
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: ${cssVar.borderRadiusLG};
|
||||
overflow: hidden;
|
||||
@@ -54,7 +54,7 @@ const useStyle = createStyles(({ cssVar }) => ({
|
||||
inset-inline-end: 20px;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-size: ${cssVar.fontSize};
|
||||
`,
|
||||
mvp: css`
|
||||
margin-inline-end: ${cssVar.marginMD};
|
||||
@@ -100,23 +100,27 @@ const locales = {
|
||||
|
||||
const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
const { styles } = useStyle();
|
||||
const [locale] = useLocale(locales);
|
||||
const meta = useRouteMeta();
|
||||
|
||||
const mermaidCode = useMermaidCode(data);
|
||||
|
||||
const cancelledRef = useRef<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
cancelledRef.current = false;
|
||||
|
||||
const renderChart = async () => {
|
||||
if (!chartRef.current || !mermaidCode) return;
|
||||
if (!chartRef.current || !mermaidCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const mermaidModule = await import('mermaid');
|
||||
const mermaid = mermaidModule.default;
|
||||
const mermaid = (await import('mermaid')).default;
|
||||
|
||||
if (isCancelled) return;
|
||||
if (cancelledRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
@@ -130,18 +134,15 @@ const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
|
||||
},
|
||||
});
|
||||
|
||||
let mermaidChartCounter = 0;
|
||||
mermaidChartCounter += 1;
|
||||
const id = `mermaid-${Date.now()}-${mermaidChartCounter}`;
|
||||
const id = `mermaid-${Date.now()}`;
|
||||
|
||||
const { svg } = await mermaid.render(id, mermaidCode);
|
||||
|
||||
if (!isCancelled && chartRef.current) {
|
||||
if (!cancelledRef.current && chartRef.current) {
|
||||
chartRef.current.innerHTML = svg;
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCancelled && chartRef.current) {
|
||||
console.error('Mermaid render error:', error);
|
||||
} catch {
|
||||
if (!cancelledRef.current && chartRef.current) {
|
||||
chartRef.current.innerHTML = 'Render Error';
|
||||
}
|
||||
}
|
||||
@@ -150,7 +151,7 @@ const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
|
||||
renderChart();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
cancelledRef.current = true;
|
||||
};
|
||||
}, [mermaidCode]);
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { FC } from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { Skeleton } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import type { BehaviorMapProps } from './BehaviorMap';
|
||||
|
||||
const InternalBehaviorMap = React.lazy(() => import('./BehaviorMap'));
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
fallback: css`
|
||||
width: 100%;
|
||||
> * {
|
||||
@@ -32,7 +32,6 @@ const locales = {
|
||||
};
|
||||
|
||||
const BehaviorMapFallback: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
const [locale] = useLocale(locales);
|
||||
return (
|
||||
<div className={styles.fallback}>
|
||||
|
||||
@@ -2,50 +2,45 @@ import { useMemo } from 'react';
|
||||
|
||||
import type { BehaviorMapItem } from './BehaviorMap';
|
||||
|
||||
export const useMermaidCode = (data: BehaviorMapItem): string => {
|
||||
const generateMermaidCode = (root: BehaviorMapItem): string => {
|
||||
const lines: string[] = [];
|
||||
const generateMermaidCode = (root: BehaviorMapItem) => {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('graph LR');
|
||||
lines.push('graph LR');
|
||||
|
||||
lines.push(`classDef baseNode fill:#fff,stroke:none,stroke-width:0px,rx:5,ry:5,font-size:14px`);
|
||||
lines.push(`classDef baseNode fill:#fff,stroke:none,stroke-width:0px,rx:5,ry:5,font-size:14px`);
|
||||
|
||||
const traverse = (node: BehaviorMapItem, parentId?: string) => {
|
||||
const safeId = `node_${node.id.replace(/[^a-z0-9]/gi, '_')}`;
|
||||
let labelText = node.label.replace(/"/g, "'");
|
||||
const traverse = (node: BehaviorMapItem, parentId?: string) => {
|
||||
const safeId = `node_${node.id.replace(/[^a-z0-9]/gi, '_')}`;
|
||||
let labelText = node.label.replace(/"/g, "'");
|
||||
|
||||
if (!parentId) {
|
||||
lines.push(`style ${safeId} font-size:16px`);
|
||||
labelText = `**${labelText}**`;
|
||||
} else if (node.targetType === 'mvp') {
|
||||
const blueDot = `<span style="display:inline-block;width:8px;height:8px;background-color:rgb(22, 119, 255);border-radius:50%;margin-right:8px;vertical-align:middle;"></span>`;
|
||||
labelText = `${blueDot}${labelText}`;
|
||||
} else if (node.targetType === 'extension') {
|
||||
const grayDot = `<span style="display:inline-block;width:8px;height:8px;background-color:rgb(160, 160, 160);border-radius:50%;margin-right:8px;vertical-align:middle;"></span>`;
|
||||
labelText = `${grayDot}${labelText}`;
|
||||
}
|
||||
lines.push(`${safeId}["${labelText}"]:::baseNode`);
|
||||
if (!parentId) {
|
||||
lines.push(`style ${safeId} font-size:16px`);
|
||||
labelText = `**${labelText}**`;
|
||||
} else if (node.targetType === 'mvp') {
|
||||
const blueDot = `<span style="display:inline-block;width:8px;height:8px;background-color:rgb(22, 119, 255);border-radius:50%;margin-inline-end:8px;vertical-align:middle;"></span>`;
|
||||
labelText = `${blueDot}${labelText}`;
|
||||
} else if (node.targetType === 'extension') {
|
||||
const grayDot = `<span style="display:inline-block;width:8px;height:8px;background-color:rgb(160, 160, 160);border-radius:50%;margin-inline-end:8px;vertical-align:middle;"></span>`;
|
||||
labelText = `${grayDot}${labelText}`;
|
||||
}
|
||||
lines.push(`${safeId}["${labelText}"]:::baseNode`);
|
||||
|
||||
if (node.link) {
|
||||
lines.push(`click ${safeId} "#${node.link}"`);
|
||||
}
|
||||
if (node.link) {
|
||||
lines.push(`click ${safeId} "#${node.link}"`);
|
||||
}
|
||||
|
||||
if (parentId) {
|
||||
lines.push(`${parentId} --> ${safeId}`);
|
||||
}
|
||||
if (parentId) {
|
||||
lines.push(`${parentId} --> ${safeId}`);
|
||||
}
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach((child) => traverse(child, safeId));
|
||||
}
|
||||
};
|
||||
|
||||
traverse(root);
|
||||
return lines.join('\n');
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach((child) => traverse(child, safeId));
|
||||
}
|
||||
};
|
||||
|
||||
const mermaidCode = useMemo(() => {
|
||||
return generateMermaidCode(data);
|
||||
}, [data]);
|
||||
|
||||
return mermaidCode;
|
||||
traverse(root);
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
export const useMermaidCode = (data: BehaviorMapItem) => {
|
||||
return useMemo(() => generateMermaidCode(data), [data]);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ const locales = {
|
||||
},
|
||||
};
|
||||
|
||||
const BezierVisualizer = (props: BezierVisualizerProps) => {
|
||||
const BezierVisualizer: React.FC<BezierVisualizerProps> = (props) => {
|
||||
const { value } = props;
|
||||
const [locale] = useLocale(locales);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
browserMockup: css`
|
||||
position: relative;
|
||||
border-top: 2em solid rgba(230, 230, 230, 0.7);
|
||||
@@ -49,7 +49,6 @@ const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
}));
|
||||
|
||||
const BrowserFrame: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const { styles } = useStyle();
|
||||
return <div className={styles.browserMockup}>{children}</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ const useStyle = createStyles(({ cssVar, token, css }) => ({
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
`,
|
||||
versionTag: css`
|
||||
user-select: none;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import React from 'react';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import SourceCodeEditor from 'dumi/theme-default/slots/SourceCodeEditor';
|
||||
|
||||
import LiveError from '../slots/LiveError';
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => {
|
||||
const styles = createStaticStyles(({ cssVar, css }) => {
|
||||
return {
|
||||
editor: css`
|
||||
// override dumi editor styles
|
||||
@@ -54,7 +54,6 @@ const LiveCode: FC<
|
||||
error: Error | null;
|
||||
} & Pick<ComponentProps<typeof SourceCodeEditor>, 'lang' | 'initialValue' | 'onChange'>
|
||||
> = (props) => {
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<div className={styles.editor}>
|
||||
<SourceCodeEditor
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { StyleProvider } from '@ant-design/cssinjs';
|
||||
import { ConfigProvider, Flex, Skeleton, Spin } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { useLocation } from 'dumi';
|
||||
|
||||
import { Common } from './styles';
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => {
|
||||
const styles = createStaticStyles(({ css, cssVar }) => {
|
||||
return {
|
||||
skeletonWrapper: css`
|
||||
width: 100%;
|
||||
@@ -27,8 +27,6 @@ const useStyle = createStyles(({ css, cssVar }) => {
|
||||
const Loading: React.FC = () => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
let loadingNode: React.ReactNode = null;
|
||||
|
||||
if (
|
||||
|
||||
@@ -5,36 +5,35 @@ import { clsx } from 'clsx';
|
||||
const useStyle = createStyles(({ cssVar, cx }) => {
|
||||
const duration = cssVar.motionDurationSlow;
|
||||
|
||||
const marker = css`
|
||||
--mark-border-size: 1px;
|
||||
position: absolute;
|
||||
border: var(--mark-border-size) solid ${cssVar.colorWarning};
|
||||
box-sizing: border-box;
|
||||
z-index: 999999;
|
||||
pointer-events: none;
|
||||
left: calc(var(--rect-left) * 1px - var(--mark-border-size));
|
||||
top: calc(var(--rect-top) * 1px - var(--mark-border-size));
|
||||
width: calc(var(--rect-width) * 1px + var(--mark-border-size) * 2);
|
||||
height: calc(var(--rect-height) * 1px + var(--mark-border-size) * 2);
|
||||
const marker = css({
|
||||
'--mark-border-size': '1px',
|
||||
position: 'absolute',
|
||||
border: `var(--mark-border-size) solid ${cssVar.colorWarning}`,
|
||||
boxSizing: 'border-box',
|
||||
zIndex: 999999,
|
||||
pointerEvents: 'none',
|
||||
left: 'calc(var(--rect-left) * 1px - var(--mark-border-size))',
|
||||
top: 'calc(var(--rect-top) * 1px - var(--mark-border-size))',
|
||||
width: 'calc(var(--rect-width) * 1px + var(--mark-border-size) * 2)',
|
||||
height: 'calc(var(--rect-height) * 1px + var(--mark-border-size) * 2)',
|
||||
opacity: 0,
|
||||
transition: `all ${duration} ease`,
|
||||
});
|
||||
|
||||
opacity: 0;
|
||||
transition: all ${duration} ease;
|
||||
`;
|
||||
const markerActive = css({
|
||||
[`&.${cx(marker)}`]: {
|
||||
opacity: 0.875,
|
||||
},
|
||||
});
|
||||
|
||||
const markerActive = css`
|
||||
&.${cx(marker)} {
|
||||
opacity: 0.85;
|
||||
}
|
||||
`;
|
||||
|
||||
const markerPrimary = css`
|
||||
&.${cx(marker)}.${cx(markerActive)} {
|
||||
--mark-border-size: 2px;
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
z-index: 1000000;
|
||||
}
|
||||
`;
|
||||
const markerPrimary = css({
|
||||
[`&.${cx(marker)}.${cx(markerActive)}`]: {
|
||||
'--mark-border-size': '2px',
|
||||
opacity: 1,
|
||||
boxShadow: '0 0 0 1px #fff',
|
||||
zIndex: 1000000,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
marker,
|
||||
|
||||
@@ -67,11 +67,14 @@ const Block: React.FC<BlockProps> = ({
|
||||
...props
|
||||
}) => {
|
||||
const divRef = React.useRef<HTMLDivElement>(null);
|
||||
const [value, setValue] = React.useState(defaultValue);
|
||||
// 多选模式下,优先使用 multipleProps 中的 defaultValue
|
||||
const multipleDefaultValue = (multipleProps as any)?.defaultValue;
|
||||
const initialValue = mode === 'single' ? defaultValue : multipleDefaultValue;
|
||||
const [value, setValue] = React.useState(initialValue);
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(defaultValue);
|
||||
}, [mode]);
|
||||
setValue(mode === 'single' ? defaultValue : multipleDefaultValue);
|
||||
}, [mode, defaultValue, multipleDefaultValue]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -101,7 +104,7 @@ const Block: React.FC<BlockProps> = ({
|
||||
options={options}
|
||||
{...(mode === 'multiple' ? multipleProps : {})}
|
||||
styles={{ popup: { zIndex: 1 } }}
|
||||
maxTagCount="responsive"
|
||||
maxTagCount={process.env.NODE_ENV === 'test' ? 1 : 'responsive'}
|
||||
placeholder="Please select"
|
||||
allowClear
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { InfoCircleOutlined, PushpinOutlined } from '@ant-design/icons';
|
||||
import { get, set } from '@rc-component/util';
|
||||
import { Button, Col, ConfigProvider, Flex, Popover, Row, Tag, theme, Typography } from 'antd';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
import Prism from 'prismjs';
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface SemanticPreviewInjectionProps {
|
||||
classNames?: Record<string, string>;
|
||||
}
|
||||
|
||||
const useStyle = createStyles(({ cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
@@ -122,6 +122,7 @@ export interface SemanticPreviewProps {
|
||||
height?: number;
|
||||
padding?: false;
|
||||
style?: React.CSSProperties;
|
||||
motion?: boolean;
|
||||
}
|
||||
|
||||
const SemanticPreview: React.FC<SemanticPreviewProps> = (props) => {
|
||||
@@ -133,6 +134,7 @@ const SemanticPreview: React.FC<SemanticPreviewProps> = (props) => {
|
||||
style,
|
||||
componentName = 'Component',
|
||||
itemsAPI,
|
||||
motion = false,
|
||||
} = props;
|
||||
const { token } = theme.useToken();
|
||||
|
||||
@@ -155,8 +157,6 @@ const SemanticPreview: React.FC<SemanticPreviewProps> = (props) => {
|
||||
|
||||
const mergedSemantic = pinSemantic || hoverSemantic;
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
const hoveredSemanticClassNames = React.useMemo(() => {
|
||||
if (!mergedSemantic) {
|
||||
return semanticClassNames;
|
||||
@@ -185,7 +185,7 @@ const SemanticPreview: React.FC<SemanticPreviewProps> = (props) => {
|
||||
className={clsx(styles.colWrap, padding === false && styles.colWrapPaddingLess)}
|
||||
style={style}
|
||||
>
|
||||
<ConfigProvider theme={{ token: { motion: false } }}>{cloneNode}</ConfigProvider>
|
||||
<ConfigProvider theme={{ token: { motion } }}>{cloneNode}</ConfigProvider>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<ul className={clsx(styles.listWrap)}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { AntDesignOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Bubble, Sender } from '@ant-design/x';
|
||||
import type { SenderRef } from '@ant-design/x/es/sender';
|
||||
import { Drawer, Flex, Typography } from 'antd';
|
||||
import type { GetProp } from 'antd';
|
||||
|
||||
@@ -28,7 +29,8 @@ export interface PromptDrawerProps {
|
||||
const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChange }) => {
|
||||
const [locale] = useLocale(locales);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const senderRef = useRef<any>(null);
|
||||
|
||||
const senderRef = useRef<SenderRef>(null);
|
||||
|
||||
const [submitPrompt, loading, prompt, resText, cancelRequest] = usePromptTheme(onThemeChange);
|
||||
|
||||
@@ -51,17 +53,21 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
|
||||
|
||||
const nextItems: GetProp<typeof Bubble.List, 'items'> = [
|
||||
{
|
||||
key: 1,
|
||||
role: 'user',
|
||||
placement: 'end',
|
||||
content: prompt,
|
||||
avatar: { icon: <UserOutlined /> },
|
||||
avatar: <UserOutlined />,
|
||||
shape: 'corner',
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
role: 'system',
|
||||
placement: 'start',
|
||||
content: resText,
|
||||
avatar: { icon: <AntDesignOutlined /> },
|
||||
avatar: <AntDesignOutlined />,
|
||||
loading: !resText,
|
||||
messageRender: (content: string) => (
|
||||
contentRender: (content: string) => (
|
||||
<Typography>
|
||||
<pre style={{ margin: 0 }}>{content}</pre>
|
||||
</Typography>
|
||||
@@ -71,9 +77,11 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
|
||||
|
||||
if (!loading) {
|
||||
nextItems.push({
|
||||
key: 3,
|
||||
role: 'divider',
|
||||
placement: 'start',
|
||||
content: locale.finishTips,
|
||||
avatar: { icon: <AntDesignOutlined /> },
|
||||
avatar: <AntDesignOutlined />,
|
||||
shape: 'corner',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable compat/compat */
|
||||
import { useRef, useState } from 'react';
|
||||
import { XStream } from '@ant-design/x';
|
||||
import { XStream } from '@ant-design/x-sdk';
|
||||
|
||||
import type { SiteContextProps } from '../../../theme/slots/SiteContext';
|
||||
|
||||
|
||||
@@ -61,11 +61,14 @@ const Block: React.FC<BlockProps> = ({
|
||||
...props
|
||||
}) => {
|
||||
const divRef = React.useRef<HTMLDivElement>(null);
|
||||
const [value, setValue] = React.useState(defaultValue);
|
||||
// 多选模式下,优先使用 multipleProps 中的 defaultValue
|
||||
const multipleDefaultValue = (multipleProps as any)?.defaultValue;
|
||||
const initialValue = mode === 'single' ? defaultValue : multipleDefaultValue;
|
||||
const [value, setValue] = React.useState(initialValue);
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(defaultValue);
|
||||
}, [mode]);
|
||||
setValue(mode === 'single' ? defaultValue : multipleDefaultValue);
|
||||
}, [mode, defaultValue, multipleDefaultValue]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -93,7 +96,7 @@ const Block: React.FC<BlockProps> = ({
|
||||
treeData={treeData}
|
||||
{...(mode === 'multiple' ? multipleProps : {})}
|
||||
styles={{ popup: { zIndex: 1 } }}
|
||||
maxTagCount="responsive"
|
||||
maxTagCount={process.env.NODE_ENV === 'test' ? 1 : 'responsive'}
|
||||
placeholder="Please select"
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import EN from './en-US.md';
|
||||
@@ -7,7 +7,7 @@ import CN from './zh-CN.md';
|
||||
|
||||
const changeLog = { cn: CN, en: EN };
|
||||
|
||||
const useStyle = createStyles(({ css }) => ({
|
||||
const classNames = createStaticStyles(({ css }) => ({
|
||||
container: css`
|
||||
max-height: max(62vh, 500px);
|
||||
overflow-y: scroll;
|
||||
@@ -25,13 +25,11 @@ const useStyle = createStyles(({ css }) => ({
|
||||
const ChangeLog = () => {
|
||||
const [, lang] = useLocale();
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
const validatedLanguage = Object.keys(changeLog).includes(lang) ? lang : 'en';
|
||||
const C = changeLog[validatedLanguage];
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={classNames.container}>
|
||||
<C />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { useSearchParams } from 'dumi';
|
||||
|
||||
import CommonHelmet from '../../common/CommonHelmet';
|
||||
import Content from '../../slots/Content';
|
||||
import Sidebar from '../../slots/Sidebar';
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
main: css`
|
||||
display: flex;
|
||||
margin-top: ${cssVar.marginXL};
|
||||
@@ -15,7 +15,6 @@ const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
}));
|
||||
|
||||
const SidebarLayout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { styles } = useStyle();
|
||||
const [searchParams] = useSearchParams();
|
||||
const hideLayout = searchParams.get('layout') === 'false';
|
||||
return (
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"app.demo.stackblitz": "Open in Stackblitz",
|
||||
"app.demo.codeblock": "Open in Hitu (This feature is only available in the internal network environment)",
|
||||
"app.demo.separate": "Open in a new window",
|
||||
"app.demo.online": "Online Address",
|
||||
"app.demo.online": "Official demo",
|
||||
"app.demo.previousVersion": "Previous version",
|
||||
"app.home.introduce": "A design system for enterprise-level products. Create an efficient and enjoyable work experience.",
|
||||
"app.home.pr-welcome": "💡 It is an alpha version and still in progress. Contribution from community is welcome!",
|
||||
"app.home.recommend": "Recommended",
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"app.demo.stackblitz": "在 Stackblitz 中打开",
|
||||
"app.demo.codeblock": "在海兔中打开(此功能仅在内网环境可用)",
|
||||
"app.demo.separate": "在新窗口打开",
|
||||
"app.demo.online": "线上地址",
|
||||
"app.demo.online": "官网示例",
|
||||
"app.demo.previousVersion": "历史版本",
|
||||
"app.home.introduce": "企业级产品设计体系,创造高效愉悦的工作体验",
|
||||
"app.home.pr-welcome": "💡 当前为 alpha 版本,仍在开发中。欢迎社区一起共建,让 Ant Design 变得更好!",
|
||||
"app.home.recommend": "精彩推荐",
|
||||
|
||||
@@ -1,242 +1,17 @@
|
||||
import { createHash } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import createEmotionServer from '@emotion/server/create-instance';
|
||||
import type { IApi, IRoute } from 'dumi';
|
||||
import ReactTechStack from 'dumi/dist/techStacks/react';
|
||||
import tsToJs from './utils/tsToJs';
|
||||
import type { IApi } from 'dumi';
|
||||
|
||||
import { dependencies, devDependencies } from '../../package.json';
|
||||
import buildAssetsPlugin from './plugins/build-assets';
|
||||
import llmsPlugin from './plugins/llms';
|
||||
import rawMdPlugin from './plugins/raw-md';
|
||||
import routesPlugin from './plugins/routes';
|
||||
import semanticMdPlugin from './plugins/semantic-md';
|
||||
import techStackPlugin from './plugins/tech-stack';
|
||||
|
||||
function extractEmotionStyle(html: string) {
|
||||
// copy from emotion ssr
|
||||
// https://github.com/vercel/next.js/blob/deprecated-main/examples/with-emotion-vanilla/pages/_document.js
|
||||
const styles = global.__ANTD_STYLE_CACHE_MANAGER_FOR_SSR__.getCacheList().map((cache) => {
|
||||
const result = createEmotionServer(cache).extractCritical(html);
|
||||
if (!result.css) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { css, ids } = result;
|
||||
|
||||
return {
|
||||
key: cache.key,
|
||||
css,
|
||||
ids,
|
||||
tag: `<style data-emotion="${cache.key} ${result.ids.join(' ')}">${result.css}</style>`,
|
||||
};
|
||||
});
|
||||
return styles.filter(Boolean);
|
||||
export default async function plugin(api: IApi) {
|
||||
techStackPlugin(api);
|
||||
routesPlugin(api);
|
||||
await buildAssetsPlugin(api);
|
||||
rawMdPlugin(api);
|
||||
semanticMdPlugin(api);
|
||||
llmsPlugin(api);
|
||||
}
|
||||
|
||||
export const getHash = (str: string, length = 8) =>
|
||||
createHash('md5').update(str).digest('hex').slice(0, length);
|
||||
|
||||
/**
|
||||
* extends dumi internal tech stack, for customize previewer props
|
||||
*/
|
||||
class AntdReactTechStack extends ReactTechStack {
|
||||
generatePreviewerProps(...[props, opts]: any) {
|
||||
props.pkgDependencyList = { ...devDependencies, ...dependencies };
|
||||
props.jsx ??= '';
|
||||
|
||||
if (opts.type === 'code-block') {
|
||||
props.jsx = opts?.entryPointCode ? tsToJs(opts.entryPointCode) : '';
|
||||
}
|
||||
|
||||
if (opts.type === 'external') {
|
||||
// try to find md file with the same name as the demo tsx file
|
||||
const locale = opts.mdAbsPath.match(/index\.([a-z-]+)\.md$/i)?.[1];
|
||||
const mdPath = opts.fileAbsPath!.replace(/\.\w+$/, '.md');
|
||||
const md = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : '';
|
||||
|
||||
const codePath = opts.fileAbsPath!.replace(/\.\w+$/, '.tsx');
|
||||
const code = fs.existsSync(codePath) ? fs.readFileSync(codePath, 'utf-8') : '';
|
||||
|
||||
props.jsx = tsToJs(code);
|
||||
|
||||
if (md) {
|
||||
// extract description & css style from md file
|
||||
const blocks: Record<string, string> = {};
|
||||
|
||||
const lines = md.split('\n');
|
||||
|
||||
let blockName = '';
|
||||
let cacheList: string[] = [];
|
||||
|
||||
// Get block name
|
||||
const getBlockName = (text: string) => {
|
||||
if (text.startsWith('## ')) {
|
||||
return text.replace('## ', '').trim();
|
||||
}
|
||||
|
||||
if (text.startsWith('```css') || text.startsWith('<style>')) {
|
||||
return 'style';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Fill block content
|
||||
const fillBlock = (name: string, lineList: string[]) => {
|
||||
if (lineList.length) {
|
||||
let fullText: string;
|
||||
|
||||
if (name === 'style') {
|
||||
fullText = lineList
|
||||
.join('\n')
|
||||
.replace(/<\/?style>/g, '')
|
||||
.replace(/```(\s*css)/g, '');
|
||||
} else {
|
||||
fullText = lineList.slice(1).join('\n');
|
||||
}
|
||||
|
||||
blocks[name] = fullText;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Mark as new block
|
||||
const nextBlockName = getBlockName(line);
|
||||
if (nextBlockName) {
|
||||
fillBlock(blockName, cacheList);
|
||||
|
||||
// Next Block
|
||||
blockName = nextBlockName;
|
||||
cacheList = [line];
|
||||
} else {
|
||||
cacheList.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Last block
|
||||
fillBlock(blockName, cacheList);
|
||||
|
||||
props.description = blocks[locale];
|
||||
props.style = blocks.style;
|
||||
}
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
const resolve = (p: string): string => require.resolve(p);
|
||||
|
||||
const RoutesPlugin = async (api: IApi) => {
|
||||
const chalk = await import('chalk').then((m) => m.default);
|
||||
// const ssrCssFileName = `ssr-${Date.now()}.css`;
|
||||
|
||||
const writeCSSFile = (key: string, hashKey: string, cssString: string) => {
|
||||
const fileName = `style-${key}.${getHash(hashKey)}.css`;
|
||||
|
||||
const filePath = path.join(api.paths.absOutputPath, fileName);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
api.logger.event(chalk.grey(`write to: ${filePath}`));
|
||||
fs.writeFileSync(filePath, cssString, 'utf8');
|
||||
}
|
||||
|
||||
return fileName;
|
||||
};
|
||||
|
||||
const addLinkStyle = (html: string, cssFile: string, prepend = false) => {
|
||||
const prefix = api.userConfig.publicPath || api.config.publicPath;
|
||||
|
||||
if (prepend) {
|
||||
return html.replace('<head>', `<head><link rel="stylesheet" href="${prefix + cssFile}">`);
|
||||
}
|
||||
|
||||
return html.replace('</head>', `<link rel="stylesheet" href="${prefix + cssFile}"></head>`);
|
||||
};
|
||||
|
||||
api.registerTechStack(() => new AntdReactTechStack());
|
||||
|
||||
api.modifyRoutes((routes) => {
|
||||
// TODO: append extra routes, such as home, changelog, form-v3
|
||||
|
||||
/**
|
||||
* **important!** Make sure that the `id` and `path` are consistent.
|
||||
* see: https://github.com/ant-design/ant-design/issues/55960
|
||||
*/
|
||||
const extraRoutesList: IRoute[] = [
|
||||
{
|
||||
id: 'changelog',
|
||||
path: 'changelog',
|
||||
absPath: '/changelog',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../CHANGELOG.en-US.md'),
|
||||
},
|
||||
{
|
||||
id: 'changelog-cn',
|
||||
path: 'changelog-cn',
|
||||
absPath: '/changelog-cn',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../CHANGELOG.zh-CN.md'),
|
||||
},
|
||||
{
|
||||
id: 'components/changelog',
|
||||
path: 'components/changelog',
|
||||
absPath: '/components/changelog',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../CHANGELOG.en-US.md'),
|
||||
},
|
||||
{
|
||||
id: 'components/changelog-cn',
|
||||
path: 'components/changelog-cn',
|
||||
absPath: '/components/changelog-cn',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../CHANGELOG.zh-CN.md'),
|
||||
},
|
||||
];
|
||||
|
||||
extraRoutesList.forEach((itemRoute) => {
|
||||
routes[itemRoute.path] = itemRoute;
|
||||
});
|
||||
|
||||
return routes;
|
||||
});
|
||||
|
||||
api.modifyExportHTMLFiles((files) =>
|
||||
files
|
||||
// exclude dynamic route path, to avoid deploy failed by `:id` directory
|
||||
.filter((f) => !f.path.includes(':'))
|
||||
.map((file) => {
|
||||
// 1. 提取 antd-style 样式
|
||||
const styles = extractEmotionStyle(file.content);
|
||||
|
||||
// 2. 提取每个样式到独立 css 文件
|
||||
styles.forEach((result) => {
|
||||
api.logger.event(
|
||||
`${chalk.yellow(file.path)} include ${chalk.blue`[${result!.key}]`} ${chalk.yellow(
|
||||
result!.ids.length,
|
||||
)} styles`,
|
||||
);
|
||||
|
||||
const cssFile = writeCSSFile(result!.key, result!.ids.join(''), result!.css);
|
||||
|
||||
file.content = addLinkStyle(file.content, cssFile);
|
||||
});
|
||||
|
||||
return file;
|
||||
}),
|
||||
);
|
||||
|
||||
// add ssr css file to html
|
||||
api.modifyConfig((memo) => {
|
||||
memo.styles ??= [];
|
||||
// memo.styles.push(`/${ssrCssFileName}`);
|
||||
|
||||
return memo;
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
api.addEntryImportsAhead(() => ({
|
||||
source: path.join(api.paths.cwd, 'components', 'style', 'antd.css'),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
export default RoutesPlugin;
|
||||
|
||||
98
.dumi/theme/plugins/build-assets.ts
Normal file
98
.dumi/theme/plugins/build-assets.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { createHash } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import createEmotionServer from '@emotion/server/create-instance';
|
||||
import type { IApi } from 'dumi';
|
||||
|
||||
export const getHash = (str: string, length = 8) =>
|
||||
createHash('md5').update(str).digest('hex').slice(0, length);
|
||||
|
||||
function extractEmotionStyle(html: string) {
|
||||
// copy from emotion ssr
|
||||
// https://github.com/vercel/next.js/blob/deprecated-main/examples/with-emotion-vanilla/pages/_document.js
|
||||
const styles = global.__ANTD_STYLE_CACHE_MANAGER_FOR_SSR__.getCacheList().map((cache) => {
|
||||
const result = createEmotionServer(cache).extractCritical(html);
|
||||
if (!result.css) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { css, ids } = result;
|
||||
|
||||
return {
|
||||
key: cache.key,
|
||||
css,
|
||||
ids,
|
||||
tag: `<style data-emotion="${cache.key} ${result.ids.join(' ')}">${result.css}</style>`,
|
||||
};
|
||||
});
|
||||
return styles.filter(Boolean);
|
||||
}
|
||||
|
||||
export default async function buildAssetsPlugin(api: IApi) {
|
||||
const chalk = await import('chalk').then((m) => m.default);
|
||||
|
||||
const writeCSSFile = (key: string, hashKey: string, cssString: string) => {
|
||||
const fileName = `style-${key}.${getHash(hashKey)}.css`;
|
||||
|
||||
const filePath = path.join(api.paths.absOutputPath, fileName);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
api.logger.event(chalk.grey(`write to: ${filePath}`));
|
||||
fs.writeFileSync(filePath, cssString, 'utf8');
|
||||
}
|
||||
|
||||
return fileName;
|
||||
};
|
||||
|
||||
const addLinkStyle = (html: string, cssFile: string, prepend = false) => {
|
||||
const prefix = api.userConfig.publicPath || api.config.publicPath;
|
||||
|
||||
if (prepend) {
|
||||
return html.replace('<head>', `<head><link rel="stylesheet" href="${prefix + cssFile}">`);
|
||||
}
|
||||
|
||||
return html.replace('</head>', `<link rel="stylesheet" href="${prefix + cssFile}"></head>`);
|
||||
};
|
||||
|
||||
api.modifyExportHTMLFiles((files) =>
|
||||
files
|
||||
// exclude dynamic route path, to avoid deploy failed by `:id` directory
|
||||
.filter((f) => !f.path.includes(':'))
|
||||
.map((file) => {
|
||||
// 1. 提取 antd-style 样式
|
||||
const styles = extractEmotionStyle(file.content);
|
||||
|
||||
// 2. 提取每个样式到独立 css 文件
|
||||
styles.forEach((result) => {
|
||||
api.logger.event(
|
||||
`${chalk.yellow(file.path)} include ${chalk.blue`[${result!.key}]`} ${chalk.yellow(
|
||||
result!.ids.length,
|
||||
)} styles`,
|
||||
);
|
||||
|
||||
const cssFile = writeCSSFile(result!.key, result!.ids.join(''), result!.css);
|
||||
|
||||
file.content = addLinkStyle(file.content, cssFile);
|
||||
});
|
||||
|
||||
return file;
|
||||
}),
|
||||
);
|
||||
|
||||
// add ssr css file to html
|
||||
api.modifyConfig((memo) => {
|
||||
memo.styles ??= [];
|
||||
|
||||
return memo;
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// `addEntryImportsAhead` do not support compile,
|
||||
// so it will build file content directly without compile.
|
||||
// We add additional pre-site script for this,
|
||||
// but this will not affect normal developer usage.
|
||||
api.addEntryImportsAhead(() => ({
|
||||
source: path.join(api.paths.cwd, 'components', 'style', '~antd.layer.css'),
|
||||
}));
|
||||
}
|
||||
}
|
||||
284
.dumi/theme/plugins/llms.ts
Normal file
284
.dumi/theme/plugins/llms.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { IApi } from 'dumi';
|
||||
import { glob } from 'glob';
|
||||
|
||||
interface DocItem {
|
||||
title: string;
|
||||
url: string;
|
||||
category: 'docs' | 'component' | 'semantic';
|
||||
content?: string;
|
||||
}
|
||||
|
||||
interface ProcessResult {
|
||||
docs: DocItem[];
|
||||
components: DocItem[];
|
||||
semantics: DocItem[];
|
||||
}
|
||||
|
||||
function processMarkdownFile(
|
||||
markdownFile: string,
|
||||
siteDir: string,
|
||||
targetArrays: ProcessResult,
|
||||
): void {
|
||||
const mdPath = path.join(siteDir, markdownFile);
|
||||
|
||||
const content = fs.readFileSync(mdPath, 'utf-8').trim();
|
||||
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract title from first H1 heading
|
||||
const titleMatch = content.match(/^#\s+(.+)$/m);
|
||||
const title = titleMatch ? titleMatch[1].trim() : path.basename(markdownFile, '.md');
|
||||
|
||||
// Generate URL from file path (use .md suffix)
|
||||
let urlPath = markdownFile.replace(/\.md$/, '');
|
||||
// Remove /index suffix for component pages
|
||||
if (urlPath.endsWith('/index')) {
|
||||
urlPath = urlPath.replace(/\/index$/, '');
|
||||
}
|
||||
const url = `https://ant.design/${urlPath}.md`;
|
||||
|
||||
// Categorize files
|
||||
if (/\/semantic.*\.md$/.test(markdownFile)) {
|
||||
// Component semantic files
|
||||
const componentName = path.basename(path.dirname(markdownFile));
|
||||
const semanticFileName = path.basename(markdownFile, '.md');
|
||||
// 提取 semantic 后缀(如 semantic_ribbon -> Ribbon)
|
||||
const semanticSuffix = semanticFileName.replace(/^semantic/, '').replace(/^_/, '');
|
||||
const displaySuffix = semanticSuffix
|
||||
? semanticSuffix
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('')
|
||||
: '';
|
||||
targetArrays.semantics.push({
|
||||
title: `${componentName}${displaySuffix ? ` ${displaySuffix}` : ''} Semantic`,
|
||||
url,
|
||||
category: 'semantic',
|
||||
content,
|
||||
});
|
||||
} else if (markdownFile.startsWith('components/')) {
|
||||
// Component documentation files
|
||||
const componentName = path.basename(markdownFile, '.md');
|
||||
targetArrays.components.push({
|
||||
title: componentName,
|
||||
url,
|
||||
category: 'component',
|
||||
content,
|
||||
});
|
||||
} else if (markdownFile.startsWith('docs/') || markdownFile.startsWith('changelog')) {
|
||||
// Documentation files
|
||||
targetArrays.docs.push({
|
||||
title,
|
||||
url,
|
||||
category: 'docs',
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function generateLLms(api: IApi) {
|
||||
const siteDir = api.paths.absOutputPath;
|
||||
|
||||
// Ensure siteDir exists
|
||||
if (!fs.existsSync(siteDir)) {
|
||||
api.logger.error('Error: Output directory does not exist. Please run build first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find all markdown files in _site, excluding llms files
|
||||
const markdownFiles = await glob('**/*.md', {
|
||||
cwd: siteDir,
|
||||
ignore: ['llms*.md', 'llms*.txt'],
|
||||
});
|
||||
|
||||
// Separate English and Chinese docs
|
||||
const englishDocs = markdownFiles.filter(
|
||||
(file) => !file.includes('-cn/') && !file.endsWith('-cn.md'),
|
||||
);
|
||||
|
||||
const chineseDocs = markdownFiles.filter(
|
||||
(file) => file.includes('-cn/') || file.endsWith('-cn.md'),
|
||||
);
|
||||
|
||||
const englishResult: ProcessResult = {
|
||||
docs: [],
|
||||
components: [],
|
||||
semantics: [],
|
||||
};
|
||||
|
||||
const chineseResult: ProcessResult = {
|
||||
docs: [],
|
||||
components: [],
|
||||
semantics: [],
|
||||
};
|
||||
|
||||
// Process English docs
|
||||
for (const markdownFile of englishDocs) {
|
||||
try {
|
||||
processMarkdownFile(markdownFile, siteDir, englishResult);
|
||||
} catch (error) {
|
||||
api.logger.warn(`Error processing ${markdownFile}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Process Chinese docs
|
||||
for (const markdownFile of chineseDocs) {
|
||||
try {
|
||||
processMarkdownFile(markdownFile, siteDir, chineseResult);
|
||||
} catch (error) {
|
||||
api.logger.warn(`Error processing ${markdownFile}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const { docs, components, semantics } = englishResult;
|
||||
const { docs: docsCn, components: componentsCn, semantics: semanticsCn } = chineseResult;
|
||||
|
||||
// Sort by title
|
||||
docs.sort((a, b) => a.title.localeCompare(b.title));
|
||||
components.sort((a, b) => a.title.localeCompare(b.title));
|
||||
semantics.sort((a, b) => a.title.localeCompare(b.title));
|
||||
docsCn.sort((a, b) => a.title.localeCompare(b.title));
|
||||
componentsCn.sort((a, b) => a.title.localeCompare(b.title));
|
||||
semanticsCn.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
// 1. Generate llms-semantic.md
|
||||
const semanticContent = [
|
||||
'# Ant Design Semantic Documentation',
|
||||
'',
|
||||
'This file contains aggregated semantic descriptions for all components.',
|
||||
'',
|
||||
`> Total ${semantics.length} components contain semantic descriptions`,
|
||||
'',
|
||||
...semantics.flatMap((semantic) => [
|
||||
`# ${semantic.title}`,
|
||||
'',
|
||||
`Source: ${semantic.url}`,
|
||||
'',
|
||||
semantic.content || '',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
]),
|
||||
].join('\n');
|
||||
|
||||
// 2. Generate llms-semantic-cn.md
|
||||
const semanticContentCn = [
|
||||
'# Ant Design 组件语义化描述',
|
||||
'',
|
||||
'本文档包含了 Ant Design 组件库中所有组件的语义化描述信息。',
|
||||
'',
|
||||
`> 总计 ${semanticsCn.length} 个组件包含语义化描述`,
|
||||
'',
|
||||
...semanticsCn.flatMap((semantic) => [
|
||||
`# ${semantic.title}`,
|
||||
'',
|
||||
`Source: ${semantic.url}`,
|
||||
'',
|
||||
semantic.content || '',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
]),
|
||||
].join('\n');
|
||||
|
||||
// 3. Generate llms-full.txt
|
||||
const fullContent = [
|
||||
'# Ant Design Component Documentation',
|
||||
'',
|
||||
'This file contains aggregated content from all component docs.',
|
||||
'',
|
||||
`> Total ${components.length} components`,
|
||||
'',
|
||||
...components.flatMap((component) => [
|
||||
`## ${component.title}`,
|
||||
'',
|
||||
`Source: ${component.url}`,
|
||||
'',
|
||||
component.content || '',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
]),
|
||||
].join('\n');
|
||||
|
||||
// 4. Generate llms-full-cn.txt
|
||||
const fullContentCn = [
|
||||
'# Ant Design 组件文档',
|
||||
'',
|
||||
'本文件包含所有组件文档的聚合内容。',
|
||||
'',
|
||||
`> 总计 ${componentsCn.length} 个组件`,
|
||||
'',
|
||||
...componentsCn.flatMap((component) => [
|
||||
`## ${component.title}`,
|
||||
'',
|
||||
`Source: ${component.url}`,
|
||||
'',
|
||||
component.content || '',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
]),
|
||||
].join('\n');
|
||||
|
||||
// 5. Generate llms.txt
|
||||
const llmsNavContent = [
|
||||
'# Ant Design - Enterprise-class React UI library',
|
||||
'',
|
||||
'- Ant Design, developed by Ant Group, is a React UI library that aims to provide a high-quality design language and development framework for enterprise-level backend management systems. It offers a rich set of components and design guidelines, helping developers build modern, responsive, and high-performance web applications.',
|
||||
'',
|
||||
'## Navigation',
|
||||
'',
|
||||
'- [Full Documentation (EN)](./llms-full.txt)',
|
||||
'- [Full Documentation (CN)](./llms-full-cn.txt)',
|
||||
'- [Semantic Documentation (EN)](./llms-semantic.md)',
|
||||
'- [Semantic Documentation (CN)](./llms-semantic-cn.md)',
|
||||
'',
|
||||
'## Docs (EN)',
|
||||
'',
|
||||
...docs.map(({ title, url }) => `- [${title}](${url})`),
|
||||
'',
|
||||
'## Docs (CN)',
|
||||
'',
|
||||
...docsCn.map(({ title, url }) => `- [${title}](${url})`),
|
||||
'',
|
||||
'## Component Docs (EN)',
|
||||
'',
|
||||
...components.map(({ title, url }) => `- [${title}](${url})`),
|
||||
'',
|
||||
'## Component Docs (CN)',
|
||||
'',
|
||||
...componentsCn.map(({ title, url }) => `- [${title}](${url})`),
|
||||
'',
|
||||
'## Semantic (EN)',
|
||||
'',
|
||||
...semantics.map(({ title, url }) => `- [${title}](${url})`),
|
||||
'',
|
||||
'## Semantic (CN)',
|
||||
'',
|
||||
...semanticsCn.map(({ title, url }) => `- [${title}](${url})`),
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
// Write all files
|
||||
fs.writeFileSync(path.join(siteDir, 'llms-semantic.md'), semanticContent);
|
||||
fs.writeFileSync(path.join(siteDir, 'llms-semantic-cn.md'), semanticContentCn);
|
||||
fs.writeFileSync(path.join(siteDir, 'llms-full.txt'), fullContent);
|
||||
fs.writeFileSync(path.join(siteDir, 'llms-full-cn.txt'), fullContentCn);
|
||||
fs.writeFileSync(path.join(siteDir, 'llms.txt'), llmsNavContent);
|
||||
|
||||
api.logger.event(
|
||||
`Generated llms.txt (navigation), llms-full.txt (${components.length} components), llms-full-cn.txt (${componentsCn.length} components), llms-semantic.md (${semantics.length} semantics), llms-semantic-cn.md (${semanticsCn.length} semantics)`,
|
||||
);
|
||||
}
|
||||
|
||||
export default async function llmsPlugin(api: IApi) {
|
||||
api.modifyExportHTMLFiles(async (files) => {
|
||||
await generateLLms(api);
|
||||
return files;
|
||||
});
|
||||
}
|
||||
596
.dumi/theme/plugins/raw-md.ts
Normal file
596
.dumi/theme/plugins/raw-md.ts
Normal file
@@ -0,0 +1,596 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { IApi } from 'dumi';
|
||||
|
||||
type TokenMeta = {
|
||||
components?: Record<
|
||||
string,
|
||||
Array<{ token: string; desc?: string; descEn?: string; type?: string }>
|
||||
>;
|
||||
global?: Record<
|
||||
string,
|
||||
{
|
||||
desc?: string;
|
||||
descEn?: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
nameEn?: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
type TokenData = Record<string, { component?: Record<string, unknown>; global?: string[] }>;
|
||||
|
||||
/**
|
||||
* 路由信息
|
||||
*/
|
||||
interface RouteInfo {
|
||||
absPath: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容过滤器上下文信息
|
||||
*/
|
||||
interface ContentFilterContext {
|
||||
route: RouteInfo;
|
||||
file: string;
|
||||
absPath: string;
|
||||
relPath: string;
|
||||
api: IApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件配置选项
|
||||
*/
|
||||
export interface PluginOptions {
|
||||
/**
|
||||
* 路由过滤器:决定哪些路由需要生成 markdown
|
||||
* @param route - 路由信息
|
||||
* @returns 返回 true 表示处理该路由,false 表示跳过
|
||||
*/
|
||||
routeFilter?: (route: RouteInfo) => boolean;
|
||||
|
||||
/**
|
||||
* 内容替换器数组:在处理内容前可以替换或修改内容,按顺序链式应用
|
||||
* @param content - 原始 markdown 内容
|
||||
* @param context - 替换器上下文信息,包含路由、文件路径、API 等
|
||||
* @returns 返回处理后的内容,如果返回 null 或空字符串则跳过该路由
|
||||
*/
|
||||
contentReplacers?: Array<(content: string, context: ContentFilterContext) => string | null>;
|
||||
|
||||
/**
|
||||
* 是否启用替换 <code src> 标签功能,默认为 true
|
||||
*/
|
||||
enableReplaceCodeSrc?: boolean;
|
||||
|
||||
/**
|
||||
* 代码追加函数:在替换 <code src> 标签时,用于追加额外的内容(如 demo 描述信息)
|
||||
* @param docFileAbs - 文档文件的绝对路径
|
||||
* @returns 返回要追加的 markdown 内容字符串
|
||||
*/
|
||||
codeAppend?: (docFileAbs: string, src: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集的 raw markdown 路由列表
|
||||
*/
|
||||
let RAW_MD_ROUTES: Array<RouteInfo> = [];
|
||||
|
||||
/**
|
||||
* 插件配置
|
||||
*/
|
||||
let PLUGIN_OPTIONS: PluginOptions = {};
|
||||
|
||||
/**
|
||||
* 记录 raw markdown 文档是否已经输出过
|
||||
*/
|
||||
let RAW_MD_EMITTED = false;
|
||||
|
||||
/**
|
||||
* Token 数据缓存
|
||||
* 避免重复读取文件,提升性能
|
||||
*/
|
||||
let TOKEN_CACHE: { meta: TokenMeta; data: TokenData } | null | undefined;
|
||||
|
||||
/**
|
||||
* 读取 JSON 文件,如果文件不存在或解析失败则返回 null
|
||||
*
|
||||
* @param abs - JSON 文件的绝对路径
|
||||
* @returns 解析后的 JSON 对象,如果文件不存在或解析失败则返回 null
|
||||
*/
|
||||
function readJsonIfExists<T>(abs: string): T | null {
|
||||
try {
|
||||
if (!fs.existsSync(abs)) return null;
|
||||
return JSON.parse(fs.readFileSync(abs, 'utf-8')) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文档文件路径检测文档语言
|
||||
* 通过文件名后缀判断是中文还是英文文档
|
||||
*
|
||||
* @param docFileAbs - 文档文件的绝对路径
|
||||
* @returns 返回检测到的语言,默认为 'en-US'
|
||||
*/
|
||||
function detectDocLocale(docFileAbs: string): 'zh-CN' | 'en-US' {
|
||||
if (/-cn\.md$/i.test(docFileAbs) || /\.zh-CN\.md$/i.test(docFileAbs)) {
|
||||
return 'zh-CN';
|
||||
}
|
||||
return 'en-US';
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换 markdown 中的 prettier-ignore 注释为空
|
||||
* 用于清理文档内容,移除 `<!-- prettier-ignore -->` 格式的注释
|
||||
*
|
||||
* @param md - 原始 markdown 内容
|
||||
* @returns 替换 prettier-ignore 注释后的 markdown 内容
|
||||
*/
|
||||
function replacePrettierIgnore(md: string) {
|
||||
return md.replace(/<!--\s*prettier-ignore\s*-->\s*\n?/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换 markdown 中的 "Semantic DOM" 部分的 code 标签为指向生成的 semantic.md 文件的链接
|
||||
*
|
||||
* @param md - 原始 markdown 内容
|
||||
* @param context - 内容过滤器上下文信息
|
||||
* @returns 替换后的 markdown 内容
|
||||
*/
|
||||
function replaceSemanticDomSection(md: string, context: ContentFilterContext) {
|
||||
// 从文档路径推断组件路径(用于生成链接)
|
||||
// 例如:components/card/index.en-US.md -> components/card/semantic.md
|
||||
const componentPathMatch = context.file.match(/components\/([^/]+)\//);
|
||||
if (!componentPathMatch) return md;
|
||||
|
||||
const componentName = componentPathMatch[1];
|
||||
const isZhCN = /-cn\.md$/i.test(context.file) || /\.zh-CN\.md$/i.test(context.file);
|
||||
const componentPath = `components/${componentName}${isZhCN ? '-cn' : ''}`;
|
||||
|
||||
// 匹配 <code src="./demo/_semantic*.tsx"> 标签并替换为 URL 地址
|
||||
return md.replace(/<code[^>]*_semantic[^>]*>.*?<\/code>/g, (match) => {
|
||||
// 从匹配的标签中提取文件名
|
||||
const demoIndex = match.indexOf('./demo/');
|
||||
if (demoIndex === -1) return match;
|
||||
const start = demoIndex + './demo/'.length;
|
||||
const end = match.indexOf('"', start);
|
||||
if (end === -1) return match;
|
||||
const semanticFile = match.substring(start, end);
|
||||
// 生成对应的 semantic.md 文件名:_semantic.tsx -> semantic.md, _semantic_meta.tsx -> semantic_meta.md
|
||||
const semanticMdFileName = semanticFile
|
||||
.replace(/^_semantic/, 'semantic')
|
||||
.replace(/\.tsx$/, '.md');
|
||||
return `https://ant.design/${componentPath}/${semanticMdFileName}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文本中最大连续反引号的数量
|
||||
* 用于确定代码块围栏所需的反引号数量,避免代码块内部的反引号与围栏冲突
|
||||
*
|
||||
* @param text - 待检查的文本内容
|
||||
* @returns 文本中最大连续反引号的数量
|
||||
*/
|
||||
function getMaxBacktickRun(text: string) {
|
||||
let max = 0;
|
||||
const re = /`+/g;
|
||||
let m: RegExpExecArray | null = re.exec(text);
|
||||
|
||||
while (m) {
|
||||
if (m[0].length > max) max = m[0].length;
|
||||
m = re.exec(text);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将代码包装为 markdown 代码块格式
|
||||
* 自动根据代码内容中的反引号数量确定围栏长度,避免代码块溢出
|
||||
*
|
||||
* @param code - 待包装的代码内容
|
||||
* @param lang - 代码块的语言标识符(如 'tsx', 'js', 'css' 等),默认为空字符串
|
||||
* @returns 格式化后的 markdown 代码块
|
||||
*/
|
||||
function wrapFencedCode(code: string, lang = '') {
|
||||
const maxTicks = getMaxBacktickRun(code);
|
||||
const fence = '`'.repeat(Math.max(3, maxTicks + 1));
|
||||
const head = lang ? `${fence}${lang}` : fence;
|
||||
return `${head}\n${code.trimEnd()}\n${fence}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件扩展名获取代码块语言标识
|
||||
* @param filePath - 文件路径
|
||||
* @returns 代码块语言标识
|
||||
*/
|
||||
function getCodeLang(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const langMap: Record<string, string> = {
|
||||
'.tsx': 'tsx',
|
||||
'.ts': 'typescript',
|
||||
'.jsx': 'jsx',
|
||||
'.js': 'javascript',
|
||||
'.vue': 'vue',
|
||||
'.css': 'css',
|
||||
'.less': 'less',
|
||||
'.scss': 'scss',
|
||||
'.sass': 'sass',
|
||||
'.json': 'json',
|
||||
'.html': 'html',
|
||||
'.md': 'markdown',
|
||||
};
|
||||
return langMap[ext] || ext.slice(1) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 markdown 内容中提取指定语言的块
|
||||
* 用于处理多语言文档,提取特定语言版本的标题块内容
|
||||
*
|
||||
* @param docFileAbs - 文档文件的绝对路径
|
||||
* @returns 提取的指定语言块内容,如果未找到则返回整个文档的 trim 结果
|
||||
*/
|
||||
function antdCodeAppend(docFileAbs: string, src: string): string {
|
||||
const docDir = path.dirname(docFileAbs);
|
||||
const locale = detectDocLocale(docFileAbs);
|
||||
const demoAbs = path.resolve(docDir, src);
|
||||
const demoMdAbs = demoAbs.replace(path.extname(src), '.md');
|
||||
const demoMd = fs.existsSync(demoMdAbs) ? fs.readFileSync(demoMdAbs, 'utf-8') : '';
|
||||
const other = locale === 'zh-CN' ? 'en-US' : 'zh-CN';
|
||||
const re = new RegExp(
|
||||
`(^|\\n)##\\s*${locale}\\s*\\n([\\s\\S]*?)(?=\\n##\\s*${other}\\s*\\n|$)`,
|
||||
'i',
|
||||
);
|
||||
const match = demoMd.match(re);
|
||||
if (!match) return demoMd.trim();
|
||||
return (match[2] ?? '').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换 <code src> 标签为 markdown 代码块
|
||||
* 将 `<code src="./demo/basic.tsx" version="5.21.0">标题</code>` 替换为完整的 demo 代码块
|
||||
* 支持读取对应的 .md 文件作为 demo 描述,并根据文档语言提取对应语言块
|
||||
*
|
||||
* @param md - 原始 markdown 内容
|
||||
* @param docFileAbs - 文档文件的绝对路径,用于解析相对路径和检测语言
|
||||
* @param enablePickLocaleBlock - 是否启用多语言块提取,可以是布尔值或函数,默认为 true
|
||||
* @returns 替换后的 markdown 内容
|
||||
*/
|
||||
function replaceCodeSrcToMarkdown(
|
||||
md: string,
|
||||
docFileAbs: string,
|
||||
codeAppend?: (docFileAbs: string, src: string) => string,
|
||||
) {
|
||||
const docDir = path.dirname(docFileAbs);
|
||||
|
||||
// 匹配 <code src="./demo/basic.tsx">标题</code> 格式的标签
|
||||
const codeTagRE = /<code\s+[^>]*?src="([^"]+)"[^>]*>([\s\S]*?)<\/code>/g;
|
||||
|
||||
return md.replace(codeTagRE, (full, src, title) => {
|
||||
try {
|
||||
// 如果标记了 debug 属性,直接去除不显示
|
||||
if (full.includes('debug')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
const demoAbs = path.resolve(docDir, src);
|
||||
const demoTitle = String(title || '').trim() || path.basename(demoAbs);
|
||||
const code = fs.existsSync(demoAbs) ? fs.readFileSync(demoAbs, 'utf-8') : '';
|
||||
|
||||
parts.push(`### ${demoTitle}`);
|
||||
|
||||
if (codeAppend) {
|
||||
parts.push(codeAppend(docFileAbs, src));
|
||||
}
|
||||
|
||||
if (code) {
|
||||
parts.push(wrapFencedCode(code, getCodeLang(demoAbs)));
|
||||
}
|
||||
|
||||
return `${parts.join('\n\n')}\n`;
|
||||
} catch {
|
||||
return full;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 components/version 目录读取 token 数据
|
||||
* 支持懒加载和缓存,避免重复读取文件
|
||||
*
|
||||
* @param api - Dumi API 实例,用于获取项目路径
|
||||
* @returns token 元数据和数据对象,如果文件不存在则返回 null
|
||||
*/
|
||||
function loadTokenFromRepo(api: IApi) {
|
||||
if (TOKEN_CACHE !== undefined) return TOKEN_CACHE;
|
||||
|
||||
const cwd = api.paths.cwd;
|
||||
const metaPath = path.join(cwd, 'components', 'version', 'token-meta.json');
|
||||
const dataPath = path.join(cwd, 'components', 'version', 'token.json');
|
||||
|
||||
const meta = readJsonIfExists<TokenMeta>(metaPath);
|
||||
const data = readJsonIfExists<TokenData>(dataPath);
|
||||
|
||||
if (meta && data) {
|
||||
TOKEN_CACHE = { meta, data };
|
||||
} else {
|
||||
TOKEN_CACHE = null;
|
||||
}
|
||||
|
||||
return TOKEN_CACHE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义 markdown 表格单元格中的特殊字符
|
||||
* 将换行符替换为空格,转义管道符,避免破坏表格结构
|
||||
*
|
||||
* @param v - 待转义的值
|
||||
* @returns 转义后的字符串
|
||||
*/
|
||||
function escapeMdCell(v: unknown) {
|
||||
return String(v ?? '')
|
||||
.replace(/\r?\n/g, ' ')
|
||||
.replace(/\|/g, '\\|')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 token 值为字符串格式
|
||||
* 用于在 markdown 表格中显示 token 的默认值
|
||||
*
|
||||
* @param v - 待规范化的值
|
||||
* @returns 规范化后的字符串,null/undefined 返回空字符串
|
||||
*/
|
||||
function normalizeValue(v: unknown) {
|
||||
if (v === undefined || v === null) return '';
|
||||
if (typeof v === 'string') return v.trim();
|
||||
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
||||
try {
|
||||
return JSON.stringify(v);
|
||||
} catch {
|
||||
return String(v);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换 <ComponentTokenTable component="Button" /> 标签为 markdown 表格
|
||||
* 处理流程:
|
||||
* 1. 为每个组件生成组件 Token 表格(包含 token 名称、描述、类型、默认值)
|
||||
* 2. 收集所有组件的全局 Token,合并去重后生成全局 Token 表格
|
||||
* 3. 支持多组件,用逗号分隔(如 component="Button,Input")
|
||||
* 4. 原位替换原标签,保持文档顺序
|
||||
*
|
||||
* @param md - 原始 markdown 内容
|
||||
* @param context - 内容过滤器上下文信息
|
||||
* @returns 替换后的 markdown 内容
|
||||
*/
|
||||
function replaceComponentTokenTable(md: string, context: ContentFilterContext) {
|
||||
const tokens = loadTokenFromRepo(context.api);
|
||||
if (!tokens) return md;
|
||||
|
||||
const { meta: tokenMeta, data: tokenData } = tokens;
|
||||
const locale = detectDocLocale(context.file);
|
||||
|
||||
const labels =
|
||||
locale === 'zh-CN'
|
||||
? {
|
||||
componentTitle: '组件 Token',
|
||||
globalTitle: '全局 Token',
|
||||
name: 'Token 名称',
|
||||
desc: '描述',
|
||||
type: '类型',
|
||||
value: '默认值',
|
||||
}
|
||||
: {
|
||||
componentTitle: 'Component Token',
|
||||
globalTitle: 'Global Token',
|
||||
name: 'Token Name',
|
||||
desc: 'Description',
|
||||
type: 'Type',
|
||||
value: 'Default Value',
|
||||
};
|
||||
|
||||
const re =
|
||||
/<ComponentTokenTable\s+[^>]*component="([^"]+)"[^>]*(?:\/>|>(?:\s*<\/ComponentTokenTable>)?)/g;
|
||||
|
||||
return md.replace(re, (full, componentProp) => {
|
||||
const comp = String(componentProp || '').trim();
|
||||
if (!comp) return full;
|
||||
|
||||
const comps = comp
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const out: string[] = [];
|
||||
// 使用 Set 收集所有组件的全局 Token,自动去重
|
||||
const globalTokenSet = new Set<string>();
|
||||
|
||||
// 遍历每个组件,分别处理组件 Token 和收集全局 Token
|
||||
comps.forEach((comp) => {
|
||||
// 1. 处理组件 Token:为每个组件生成独立的表格
|
||||
const metaList = tokenMeta.components?.[comp];
|
||||
if (Array.isArray(metaList) && metaList.length > 0) {
|
||||
out.push(`## ${labels.componentTitle} (${comp})`);
|
||||
out.push(`| ${labels.name} | ${labels.desc} | ${labels.type} | ${labels.value} |`);
|
||||
out.push(`| --- | --- | --- | --- |`);
|
||||
|
||||
metaList.forEach((item) => {
|
||||
const name = item.token;
|
||||
// 根据文档语言选择对应的描述文本
|
||||
const desc = locale === 'zh-CN' ? (item.desc ?? '') : (item.descEn ?? item.desc ?? '');
|
||||
const type = item.type ?? '';
|
||||
// 从 tokenData 中获取组件 Token 的默认值
|
||||
const value = normalizeValue(tokenData?.[comp]?.component?.[name]);
|
||||
out.push(
|
||||
`| ${escapeMdCell(name)} | ${escapeMdCell(desc)} | ${escapeMdCell(type)} | ${escapeMdCell(
|
||||
value,
|
||||
)} |`,
|
||||
);
|
||||
});
|
||||
|
||||
out.push('');
|
||||
}
|
||||
|
||||
// 2. 收集全局 Token:从每个组件的 global 数组中收集 token 名称
|
||||
const globalTokens = tokenData?.[comp]?.global;
|
||||
if (Array.isArray(globalTokens)) {
|
||||
globalTokens.forEach((token) => {
|
||||
globalTokenSet.add(token);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 处理全局 Token:合并所有组件的全局 Token,生成统一的表格
|
||||
if (globalTokenSet.size > 0) {
|
||||
const globalTokenList = Array.from(globalTokenSet).sort();
|
||||
if (globalTokenList.length > 0) {
|
||||
out.push(`## ${labels.globalTitle}`);
|
||||
out.push(`| ${labels.name} | ${labels.desc} | ${labels.type} | ${labels.value} |`);
|
||||
out.push(`| --- | --- | --- | --- |`);
|
||||
|
||||
globalTokenList.forEach((tokenName) => {
|
||||
const meta = tokenMeta.global?.[tokenName];
|
||||
if (meta) {
|
||||
// 根据文档语言选择对应的描述文本
|
||||
const desc = locale === 'zh-CN' ? (meta.desc ?? '') : (meta.descEn ?? meta.desc ?? '');
|
||||
const type = meta.type ?? '';
|
||||
// 全局 Token 的默认值需要在运行时通过 getDesignToken() 计算
|
||||
// 在静态 markdown 生成阶段无法获取,因此留空
|
||||
const value = '';
|
||||
out.push(
|
||||
`| ${escapeMdCell(tokenName)} | ${escapeMdCell(desc)} | ${escapeMdCell(type)} | ${escapeMdCell(
|
||||
value,
|
||||
)} |`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
out.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有生成任何内容,则保留原标签
|
||||
if (!out.length) return full;
|
||||
// 返回生成的 markdown 表格,前后添加换行确保格式正确
|
||||
return `\n\n${out.join('\n').trim()}\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出处理后的 raw markdown 文件到构建输出目录
|
||||
* 仅在 production 环境下执行,确保每个文档只处理一次。
|
||||
* 对收集的路由进行处理:移除 prettier-ignore 注释、替换 `<code src>` 标签、
|
||||
* 替换 `<ComponentTokenTable />` 组件,然后将处理后的 markdown 写入输出目录
|
||||
*
|
||||
* @param api - Dumi API 实例,用于获取输出路径等配置
|
||||
*/
|
||||
function emitRawMd(api: IApi) {
|
||||
if (process.env.NODE_ENV !== 'production') return;
|
||||
if (RAW_MD_EMITTED) return;
|
||||
RAW_MD_EMITTED = true;
|
||||
|
||||
const outRoot = api.paths.absOutputPath;
|
||||
|
||||
RAW_MD_ROUTES.forEach((route) => {
|
||||
try {
|
||||
const { absPath, file } = route;
|
||||
const relPath = absPath.replace(/^\//, '');
|
||||
if (!relPath || !fs.existsSync(file)) return;
|
||||
|
||||
// 应用路由过滤器
|
||||
if (PLUGIN_OPTIONS.routeFilter && !PLUGIN_OPTIONS.routeFilter(route)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(file, 'utf-8');
|
||||
|
||||
const filterContext: ContentFilterContext = {
|
||||
route,
|
||||
file,
|
||||
absPath,
|
||||
relPath,
|
||||
api,
|
||||
};
|
||||
|
||||
if (PLUGIN_OPTIONS.contentReplacers && PLUGIN_OPTIONS.contentReplacers.length > 0) {
|
||||
for (const replacer of PLUGIN_OPTIONS.contentReplacers) {
|
||||
const replacedContent = replacer(content, filterContext);
|
||||
if (replacedContent === null || replacedContent === '') {
|
||||
return;
|
||||
}
|
||||
content = replacedContent;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理步骤:
|
||||
// 1. 替换 Semantic DOM 部分为指向生成的 semantic.md 文件的链接
|
||||
content = replaceSemanticDomSection(content, filterContext);
|
||||
// 2. 替换 <ComponentTokenTable /> 组件为 markdown 表格
|
||||
content = replaceComponentTokenTable(content, filterContext);
|
||||
// 3. 替换 prettier-ignore 注释(可通过配置开关控制)
|
||||
content = replacePrettierIgnore(content);
|
||||
|
||||
// 4. 替换 <code src> 标签为完整的代码块(可通过配置开关控制)
|
||||
if (PLUGIN_OPTIONS.enableReplaceCodeSrc !== false) {
|
||||
content = replaceCodeSrcToMarkdown(content, file, PLUGIN_OPTIONS.codeAppend);
|
||||
}
|
||||
|
||||
const outMd = path.join(outRoot, `${relPath}.md`);
|
||||
fs.mkdirSync(path.dirname(outMd), { recursive: true });
|
||||
fs.writeFileSync(outMd, content, 'utf-8');
|
||||
api.logger.event(`Build ${relPath}.md`);
|
||||
} catch (e) {
|
||||
api.logger.error(`Failed to emit raw markdown for ${route.file}:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 负责在构建过程中处理 markdown 文档,生成 flattened markdown 文件到输出目录。
|
||||
* 主要功能包括:
|
||||
* 1. 收集所有 markdown 路由
|
||||
* 2. 在 HTML 文件导出阶段输出处理后的 raw markdown 文件
|
||||
*
|
||||
* @param api - Dumi API 实例
|
||||
* @param options - 插件配置选项
|
||||
*/
|
||||
export default function rawMdPlugin(api: IApi) {
|
||||
// 注册配置键,允许用户在配置中使用 rawMd 键
|
||||
api.describe({
|
||||
key: 'rawMd',
|
||||
config: {
|
||||
schema(joi) {
|
||||
return joi.object({
|
||||
routeFilter: joi.function(),
|
||||
contentReplacers: joi.array().items(joi.function()),
|
||||
enableReplaceCodeSrc: joi.boolean(),
|
||||
codeAppend: joi.function(),
|
||||
});
|
||||
},
|
||||
onChange: api.ConfigChangeType.reload,
|
||||
},
|
||||
});
|
||||
|
||||
const configOptions = api.userConfig.rawMd as PluginOptions | undefined;
|
||||
PLUGIN_OPTIONS = configOptions || {
|
||||
enableReplaceCodeSrc: true,
|
||||
codeAppend: antdCodeAppend,
|
||||
};
|
||||
|
||||
api.modifyRoutes((routes) => {
|
||||
RAW_MD_ROUTES = Object.values(routes)
|
||||
.filter((r) => typeof r?.file === 'string' && r.file.endsWith('.md'))
|
||||
.filter((r) => typeof r?.absPath === 'string' && r.absPath && !r.absPath.includes(':'))
|
||||
.map((r) => ({ absPath: r.absPath, file: r.file as string }));
|
||||
return routes;
|
||||
});
|
||||
|
||||
api.modifyExportHTMLFiles((files) => {
|
||||
emitRawMd(api);
|
||||
return files;
|
||||
});
|
||||
}
|
||||
50
.dumi/theme/plugins/routes.ts
Normal file
50
.dumi/theme/plugins/routes.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { IApi, IRoute } from 'dumi';
|
||||
|
||||
const resolve = (p: string): string => require.resolve(p);
|
||||
|
||||
export default function routesPlugin(api: IApi) {
|
||||
api.modifyRoutes((routes) => {
|
||||
// TODO: append extra routes, such as home, changelog, form-v3
|
||||
|
||||
/**
|
||||
* **important!** Make sure that the `id` and `path` are consistent.
|
||||
* see: https://github.com/ant-design/ant-design/issues/55960
|
||||
*/
|
||||
const extraRoutesList: IRoute[] = [
|
||||
{
|
||||
id: 'changelog',
|
||||
path: 'changelog',
|
||||
absPath: '/changelog',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../../CHANGELOG.en-US.md'),
|
||||
},
|
||||
{
|
||||
id: 'changelog-cn',
|
||||
path: 'changelog-cn',
|
||||
absPath: '/changelog-cn',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../../CHANGELOG.zh-CN.md'),
|
||||
},
|
||||
{
|
||||
id: 'components/changelog',
|
||||
path: 'components/changelog',
|
||||
absPath: '/components/changelog',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../../CHANGELOG.en-US.md'),
|
||||
},
|
||||
{
|
||||
id: 'components/changelog-cn',
|
||||
path: 'components/changelog-cn',
|
||||
absPath: '/components/changelog-cn',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../../CHANGELOG.zh-CN.md'),
|
||||
},
|
||||
];
|
||||
|
||||
extraRoutesList.forEach((itemRoute) => {
|
||||
routes[itemRoute.path] = itemRoute;
|
||||
});
|
||||
|
||||
return routes;
|
||||
});
|
||||
}
|
||||
553
.dumi/theme/plugins/semantic-md.ts
Normal file
553
.dumi/theme/plugins/semantic-md.ts
Normal file
@@ -0,0 +1,553 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { IApi } from 'dumi';
|
||||
|
||||
let COMPONENT_ROUTES: Array<{ absPath: string; componentName: string; outputPath: string }> = [];
|
||||
let SEMANTIC_MD_EMITTED = false;
|
||||
|
||||
// 解析 locales 字符串片段为语义映射表
|
||||
function extractSemantics(objContent: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
const semanticMatches = objContent.matchAll(/['"]?([^'":\s]+)['"]?\s*:\s*['"]([^'"]+)['"],?/g);
|
||||
for (const match of semanticMatches) {
|
||||
const [, key, value] = match;
|
||||
if (key && value) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 _semantic*.tsx 文件中提取语义信息
|
||||
* @param semanticFile - _semantic*.tsx 文件的绝对路径
|
||||
* @returns 包含中文和英文语义描述的对象,失败返回 null
|
||||
*/
|
||||
function extractLocaleInfoFromContent(content: string): {
|
||||
cn: Record<string, string>;
|
||||
en: Record<string, string>;
|
||||
} | null {
|
||||
// 匹配 locales 对象定义
|
||||
const localesMatch = content.match(/const locales = \{([\s\S]*?)\};/);
|
||||
if (!localesMatch) return null;
|
||||
|
||||
// 提取中文和英文的语义描述
|
||||
const cnMatch = content.match(/cn:\s*\{([\s\S]*?)\},?\s*en:/);
|
||||
if (!cnMatch) return null;
|
||||
|
||||
const enMatch = content.match(/en:\s*\{([\s\S]*?)\}\s*[,;]/);
|
||||
if (!enMatch) return null;
|
||||
|
||||
const cnContent = cnMatch[1];
|
||||
const enContent = enMatch[1];
|
||||
|
||||
const cnSemantics = extractSemantics(cnContent);
|
||||
const enSemantics = extractSemantics(enContent);
|
||||
|
||||
if (Object.keys(cnSemantics).length === 0) return null;
|
||||
|
||||
return { cn: cnSemantics, en: enSemantics };
|
||||
}
|
||||
|
||||
// 根据 import 路径找到模板文件的实际路径
|
||||
function resolveTemplateFilePath(semanticFile: string, importPath: string): string | null {
|
||||
const basePath = path.resolve(path.dirname(semanticFile), importPath);
|
||||
const candidates = [
|
||||
basePath,
|
||||
`${basePath}.tsx`,
|
||||
`${basePath}.ts`,
|
||||
path.join(basePath, 'index.tsx'),
|
||||
path.join(basePath, 'index.ts'),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 识别被 JSX 使用的模板组件导入
|
||||
function parseTemplateUsage(content: string): Array<{ componentName: string; importPath: string }> {
|
||||
const results: Array<{ componentName: string; importPath: string }> = [];
|
||||
const importRegex = /import\s+([^;]+?)\s+from\s+['"]([^'"]+)['"];?/g;
|
||||
for (const match of content.matchAll(importRegex)) {
|
||||
const importClause = match[1].trim();
|
||||
const importPath = match[2].trim();
|
||||
if (!importPath.startsWith('.')) continue;
|
||||
|
||||
const componentNames: string[] = [];
|
||||
if (importClause.startsWith('{')) {
|
||||
const names = importClause
|
||||
.replace(/[{}]/g, '')
|
||||
.split(',')
|
||||
.map((name) =>
|
||||
name
|
||||
.trim()
|
||||
.split(/\s+as\s+/)[0]
|
||||
.trim(),
|
||||
)
|
||||
.filter(Boolean);
|
||||
componentNames.push(...names);
|
||||
} else if (importClause.includes('{')) {
|
||||
const [defaultName, namedPart] = importClause.split(',');
|
||||
if (defaultName?.trim()) {
|
||||
componentNames.push(defaultName.trim());
|
||||
}
|
||||
const names = namedPart
|
||||
.replace(/[{}]/g, '')
|
||||
.split(',')
|
||||
.map((name) =>
|
||||
name
|
||||
.trim()
|
||||
.split(/\s+as\s+/)[0]
|
||||
.trim(),
|
||||
)
|
||||
.filter(Boolean);
|
||||
componentNames.push(...names);
|
||||
} else {
|
||||
componentNames.push(importClause);
|
||||
}
|
||||
|
||||
for (const componentName of componentNames) {
|
||||
if (new RegExp(`<${componentName}\\b`).test(content)) {
|
||||
results.push({ componentName, importPath });
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// 解析 ignoreSemantics 属性值
|
||||
function parseIgnoreSemantics(propsString: string): string[] {
|
||||
const ignoreMatch = propsString.match(/ignoreSemantics\s*=\s*\{([\s\S]*?)\}/);
|
||||
if (!ignoreMatch) return [];
|
||||
const ignoreContent = ignoreMatch[1];
|
||||
return Array.from(ignoreContent.matchAll(/['"]([^'"]+)['"]/g)).map((match) => match[1]);
|
||||
}
|
||||
|
||||
// 解析 singleOnly 属性值
|
||||
function parseSingleOnly(propsString: string): boolean {
|
||||
const singleOnlyMatch = propsString.match(/singleOnly(\s*=\s*\{?([^}\s]+)\}?)?/);
|
||||
if (!singleOnlyMatch) return false;
|
||||
if (!singleOnlyMatch[1]) return true;
|
||||
const value = singleOnlyMatch[2];
|
||||
return value !== 'false';
|
||||
}
|
||||
|
||||
// 抽取模板组件 JSX 的属性字符串
|
||||
function extractTemplateProps(content: string, componentName: string): string {
|
||||
const start = content.indexOf(`<${componentName}`);
|
||||
if (start === -1) return '';
|
||||
let index = start + componentName.length + 1;
|
||||
const propsStart = index;
|
||||
let braceDepth = 0;
|
||||
let stringChar: string | null = null;
|
||||
|
||||
for (; index < content.length; index += 1) {
|
||||
const ch = content[index];
|
||||
const prev = content[index - 1];
|
||||
|
||||
if (stringChar) {
|
||||
if (ch === stringChar && prev !== '\\') {
|
||||
stringChar = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"' || ch === "'" || ch === '`') {
|
||||
stringChar = ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '{') {
|
||||
braceDepth += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '}') {
|
||||
if (braceDepth > 0) braceDepth -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '>' && braceDepth === 0) {
|
||||
return content.slice(propsStart, index).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// 应用 ignoreSemantics 与 singleOnly 过滤规则
|
||||
function filterSemantics(
|
||||
semantics: Record<string, string>,
|
||||
options: { ignoreSemantics: string[]; singleOnly: boolean; templatePath: string },
|
||||
): Record<string, string> {
|
||||
const ignoreSet = new Set(options.ignoreSemantics);
|
||||
const filteredEntries = Object.entries(semantics).filter(([key]) => !ignoreSet.has(key));
|
||||
|
||||
if (options.singleOnly) {
|
||||
const singleOnlyKeys = new Set(['item', 'itemContent', 'itemRemove']);
|
||||
return Object.fromEntries(filteredEntries.filter(([key]) => !singleOnlyKeys.has(key)));
|
||||
}
|
||||
|
||||
return Object.fromEntries(filteredEntries);
|
||||
}
|
||||
|
||||
// 从模板组件的 locales 提取语义信息
|
||||
function extractSemanticInfoFromTemplate(
|
||||
semanticFile: string,
|
||||
content: string,
|
||||
): { cn: Record<string, string>; en: Record<string, string> } | null {
|
||||
const templates = parseTemplateUsage(content);
|
||||
if (templates.length === 0) return null;
|
||||
|
||||
for (const template of templates) {
|
||||
const templatePath = resolveTemplateFilePath(semanticFile, template.importPath);
|
||||
if (!templatePath) continue;
|
||||
|
||||
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
||||
const templateLocales = extractLocaleInfoFromContent(templateContent);
|
||||
if (!templateLocales) continue;
|
||||
|
||||
const propsString = extractTemplateProps(content, template.componentName);
|
||||
const ignoreSemantics = parseIgnoreSemantics(propsString);
|
||||
const singleOnly = parseSingleOnly(propsString);
|
||||
|
||||
return {
|
||||
cn: filterSemantics(templateLocales.cn, {
|
||||
ignoreSemantics,
|
||||
singleOnly,
|
||||
templatePath,
|
||||
}),
|
||||
en: filterSemantics(templateLocales.en, {
|
||||
ignoreSemantics,
|
||||
singleOnly,
|
||||
templatePath,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从 _semantic*.tsx 中提取语义信息
|
||||
function extractSemanticInfo(semanticFile: string): {
|
||||
cn: Record<string, string>;
|
||||
en: Record<string, string>;
|
||||
} | null {
|
||||
try {
|
||||
if (!fs.existsSync(semanticFile)) return null;
|
||||
|
||||
const content = fs.readFileSync(semanticFile, 'utf-8');
|
||||
const localeInfo = extractLocaleInfoFromContent(content);
|
||||
if (localeInfo) return localeInfo;
|
||||
|
||||
return extractSemanticInfoFromTemplate(semanticFile, content);
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`Failed to extract semantic info from ${semanticFile}:`, error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成语义名称列表的 markdown 格式
|
||||
* @param semantics - 语义信息对象
|
||||
* @returns markdown 格式的语义名称列表
|
||||
*/
|
||||
function generateSemanticParts(semantics: Record<string, string>): string {
|
||||
const parts: string[] = [];
|
||||
for (const [name, desc] of Object.entries(semantics)) {
|
||||
// 将点号替换为连字符,匹配 DOM 中的实际 className 格式
|
||||
const className = name.replace(/\./g, '-');
|
||||
parts.push(`- ${name}(\`semantic-mark-${className}\`): ${desc}`);
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成使用案例代码模板
|
||||
* @param componentName - 组件名(如 "button", "float-button")
|
||||
* @param semanticSuffix - 语义文件后缀(如 "_group", "_input", "_search")
|
||||
* @param semantics - 语义信息对象
|
||||
* @returns 使用案例代码
|
||||
*/
|
||||
function generateUsageExample(
|
||||
componentName: string,
|
||||
semanticSuffix: string,
|
||||
semantics: Record<string, string>,
|
||||
): string {
|
||||
// 将组件名转换为 PascalCase(如 "float-button" -> "FloatButton")
|
||||
const componentDisplayName = componentName
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('');
|
||||
|
||||
// 根据后缀生成组件名
|
||||
let tagName = componentDisplayName;
|
||||
if (semanticSuffix) {
|
||||
// 移除开头的下划线,然后转换为 PascalCase
|
||||
// 如 "_group" -> "Group", "_input" -> "Input", "_search" -> "Search"
|
||||
const suffixParts = semanticSuffix
|
||||
.replace(/^_/, '')
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1));
|
||||
const subComponentName = suffixParts.join('');
|
||||
tagName = `${componentDisplayName}.${subComponentName}`;
|
||||
}
|
||||
|
||||
// 生成 classNames 对象
|
||||
const classNamesEntries: string[] = [];
|
||||
for (const [name] of Object.entries(semantics)) {
|
||||
// 将点号替换为连字符,匹配 DOM 中的实际 className 格式
|
||||
const className = name.replace(/\./g, '-');
|
||||
classNamesEntries.push(` ${name}: "semantic-mark-${className}"`);
|
||||
}
|
||||
|
||||
return `<${tagName}
|
||||
{...otherProps}
|
||||
classNames={{
|
||||
${classNamesEntries.join(',\n')}
|
||||
}}
|
||||
/>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从测试快照文件中读取组件的 HTML snapshot,并提取包含所有 semantic 元素的最小根元素
|
||||
* 注意:此函数依赖 Jest 快照文件的路径与命名约定(含 snapshot key 规则)。
|
||||
* 若测试目录结构或快照格式变化,文档构建可能失败或产生不完整结果,请同步调整此处逻辑。
|
||||
* @param semanticFile - _semantic*.tsx 文件的绝对路径
|
||||
* @param cwd - 项目根目录
|
||||
* @returns HTML 字符串,失败返回 null
|
||||
*/
|
||||
function getComponentHTMLSnapshot(semanticFile: string, cwd: string): string | null {
|
||||
try {
|
||||
const relativePath = path.relative(cwd, semanticFile);
|
||||
const pathMatch = relativePath.match(/^components\/([^/]+)\/demo\/([^/]+)\.tsx$/);
|
||||
if (!pathMatch) return null;
|
||||
|
||||
const [, componentName, fileName] = pathMatch;
|
||||
const snapshotPath = path.join(
|
||||
cwd,
|
||||
'components',
|
||||
componentName,
|
||||
'__tests__',
|
||||
'__snapshots__',
|
||||
'demo-semantic.test.tsx.snap',
|
||||
);
|
||||
|
||||
if (!fs.existsSync(snapshotPath)) return null;
|
||||
|
||||
const snapshotContent = fs.readFileSync(snapshotPath, 'utf-8');
|
||||
// 匹配快照 key:exports[`renders components/button/demo/_semantic.tsx correctly 1`] = `...`;
|
||||
const snapshotKeyPattern = `components/${componentName}/demo/${fileName}.tsx correctly`;
|
||||
const regex = new RegExp(
|
||||
`exports\\[\\\`[^\\\`]*${snapshotKeyPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\\`]*\\\`\\]\\s*=\\s*\\\`([\\s\\S]*?)\\\`;`,
|
||||
);
|
||||
const snapshotMatch = snapshotContent.match(regex);
|
||||
if (!snapshotMatch) return null;
|
||||
|
||||
let html = snapshotMatch[1].trim();
|
||||
|
||||
// 处理 JSON 格式的快照:{ type: 'demo', html: '...' }
|
||||
if (html.startsWith('{') && html.includes('"html"')) {
|
||||
try {
|
||||
const parsed = JSON.parse(html);
|
||||
if (parsed.html) {
|
||||
html = parsed.html;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const { JSDOM } = require('jsdom');
|
||||
const dom = new JSDOM(html);
|
||||
const document = dom.window.document;
|
||||
const semanticElements = document.querySelectorAll('[class*="semantic-"]');
|
||||
|
||||
if (semanticElements.length === 0) {
|
||||
return html;
|
||||
}
|
||||
|
||||
// 向上查找包含所有 semantic 元素的最小根元素(通常是组件根元素,如 button)
|
||||
const firstSemantic = semanticElements[0] as Element;
|
||||
let rootElement: Element | null = firstSemantic;
|
||||
|
||||
while (rootElement && rootElement.parentElement) {
|
||||
const parent: Element | null = rootElement.parentElement;
|
||||
// 遇到布局容器时停止,避免包含 SemanticPreview 的外层容器
|
||||
if (
|
||||
!parent ||
|
||||
parent.classList.contains('ant-row') ||
|
||||
parent.classList.contains('ant-col') ||
|
||||
parent.classList.contains('acss-') ||
|
||||
parent === document.body ||
|
||||
parent === document.documentElement
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查父元素是否包含所有 semantic 元素
|
||||
let parentContainsAll = true;
|
||||
for (let i = 0; i < semanticElements.length; i++) {
|
||||
if (!parent.contains(semanticElements[i] as Element)) {
|
||||
parentContainsAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (parentContainsAll) {
|
||||
rootElement = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rootElement ? rootElement.outerHTML : html;
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.warn(`[semantic-md] Failed to get HTML snapshot from ${semanticFile}:`, error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为每个组件生成 semantic*.md 文件
|
||||
* @param api - Dumi API 实例
|
||||
*/
|
||||
function emitSemanticMd(api: IApi) {
|
||||
if (process.env.NODE_ENV !== 'production') return;
|
||||
if (SEMANTIC_MD_EMITTED) return;
|
||||
SEMANTIC_MD_EMITTED = true;
|
||||
|
||||
const outRoot = api.paths.absOutputPath;
|
||||
const cwd = api.paths.cwd;
|
||||
|
||||
COMPONENT_ROUTES.forEach(({ componentName, outputPath, absPath }) => {
|
||||
try {
|
||||
const componentDir = path.join(cwd, 'components', componentName);
|
||||
const demoDir = path.join(componentDir, 'demo');
|
||||
|
||||
if (fs.existsSync(demoDir)) {
|
||||
const demoFiles = fs.readdirSync(demoDir);
|
||||
// 查找所有 _semantic*.tsx 文件(如 _semantic.tsx, _semantic_group.tsx)
|
||||
const semanticFiles = demoFiles.filter(
|
||||
(demoFile) => demoFile.startsWith('_semantic') && demoFile.endsWith('.tsx'),
|
||||
);
|
||||
|
||||
semanticFiles.forEach((semanticFile) => {
|
||||
const semanticFilePath = path.join(demoDir, semanticFile);
|
||||
const semanticInfo = extractSemanticInfo(semanticFilePath);
|
||||
|
||||
if (!semanticInfo) {
|
||||
if (process.env.DEBUG) {
|
||||
console.warn(
|
||||
`[semantic-md] Failed to extract semantic info from ${semanticFilePath}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成对应的 markdown 文件名:_semantic.tsx -> semantic.md, _semantic_group.tsx -> semantic_group.md
|
||||
const semanticSuffix = semanticFile.replace(/^_semantic/, '').replace(/\.tsx$/, '');
|
||||
const semanticMdFileName = `semantic${semanticSuffix}.md`;
|
||||
const semanticMdPath = path.join(outRoot, outputPath, semanticMdFileName);
|
||||
const semanticMdDir = path.dirname(semanticMdPath);
|
||||
|
||||
if (!fs.existsSync(semanticMdDir)) {
|
||||
fs.mkdirSync(semanticMdDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 根据路由路径判断语言(-cn 后缀表示中文)
|
||||
const isZhCN = absPath.includes('-cn');
|
||||
let semantics: Record<string, string> = {};
|
||||
if (semanticInfo) {
|
||||
semantics = isZhCN ? semanticInfo.cn : semanticInfo.en;
|
||||
}
|
||||
|
||||
const displayName = componentName
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('');
|
||||
|
||||
const titleSuffix = semanticSuffix
|
||||
? semanticSuffix
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('.')
|
||||
: '';
|
||||
|
||||
const parts = [
|
||||
`## ${displayName}${titleSuffix ? `${titleSuffix}` : ''}`,
|
||||
'',
|
||||
'### Semantic Parts',
|
||||
'',
|
||||
];
|
||||
|
||||
if (Object.keys(semantics).length > 0) {
|
||||
parts.push(generateSemanticParts(semantics));
|
||||
parts.push('');
|
||||
parts.push(`### ${isZhCN ? '使用案例' : 'Usage Example'}`);
|
||||
parts.push('');
|
||||
parts.push('```tsx');
|
||||
parts.push(generateUsageExample(componentName, semanticSuffix, semantics));
|
||||
parts.push('```');
|
||||
parts.push('');
|
||||
parts.push('### Abstract DOM Structure');
|
||||
parts.push('');
|
||||
|
||||
const htmlSnapshot = semanticInfo
|
||||
? getComponentHTMLSnapshot(semanticFilePath, cwd)
|
||||
: null;
|
||||
|
||||
if (htmlSnapshot) {
|
||||
parts.push('```html');
|
||||
parts.push(htmlSnapshot);
|
||||
parts.push('```');
|
||||
}
|
||||
}
|
||||
|
||||
const content = `${parts.join('\n')}\n`;
|
||||
fs.writeFileSync(semanticMdPath, content, 'utf-8');
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
api.logger.error(`Failed to generate semantic md for ${componentName}:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic markdown 生成插件
|
||||
* @param api - Dumi API 实例
|
||||
*/
|
||||
export default function semanticMdPlugin(api: IApi) {
|
||||
// 收集组件路由信息(过滤出 /components/* 和 /components/*-cn 路由)
|
||||
api.modifyRoutes((routes) => {
|
||||
COMPONENT_ROUTES = Object.values(routes)
|
||||
.filter((r) => typeof r?.absPath === 'string' && r.absPath && !r.absPath.includes(':'))
|
||||
.filter((r) => {
|
||||
const match = r.absPath.match(/^\/components\/([^/]+)\/?$/);
|
||||
return !!match;
|
||||
})
|
||||
.map((r) => {
|
||||
const match = r.absPath.match(/^\/components\/([^/]+)\/?$/);
|
||||
const fullComponentName = match![1];
|
||||
// 移除 -cn 后缀获取基础组件名
|
||||
const baseComponentName = fullComponentName.replace(/-cn$/, '');
|
||||
const outputPath = `components/${fullComponentName}`;
|
||||
|
||||
return {
|
||||
absPath: r.absPath,
|
||||
componentName: baseComponentName,
|
||||
outputPath,
|
||||
};
|
||||
});
|
||||
|
||||
return routes;
|
||||
});
|
||||
|
||||
// 在 HTML 文件导出阶段生成 semantic.md 文件
|
||||
api.modifyExportHTMLFiles((files) => {
|
||||
emitSemanticMd(api);
|
||||
return files;
|
||||
});
|
||||
}
|
||||
101
.dumi/theme/plugins/tech-stack.ts
Normal file
101
.dumi/theme/plugins/tech-stack.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import fs from 'fs';
|
||||
import type { IApi } from 'dumi';
|
||||
import ReactTechStack from 'dumi/dist/techStacks/react';
|
||||
import tsToJs from '../utils/tsToJs';
|
||||
|
||||
import { dependencies, devDependencies } from '../../../package.json';
|
||||
|
||||
/**
|
||||
* extends dumi internal tech stack, for customize previewer props
|
||||
*/
|
||||
class AntdReactTechStack extends ReactTechStack {
|
||||
generatePreviewerProps(...[props, opts]: any) {
|
||||
props.pkgDependencyList = { ...devDependencies, ...dependencies };
|
||||
props.jsx ??= '';
|
||||
|
||||
if (opts.type === 'code-block') {
|
||||
props.jsx = opts?.entryPointCode ? tsToJs(opts.entryPointCode) : '';
|
||||
}
|
||||
|
||||
if (opts.type === 'external') {
|
||||
// try to find md file with the same name as the demo tsx file
|
||||
const locale = opts.mdAbsPath.match(/index\.([a-z-]+)\.md$/i)?.[1];
|
||||
const mdPath = opts.fileAbsPath!.replace(/\.\w+$/, '.md');
|
||||
const md = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : '';
|
||||
|
||||
const codePath = opts.fileAbsPath!.replace(/\.\w+$/, '.tsx');
|
||||
const code = fs.existsSync(codePath) ? fs.readFileSync(codePath, 'utf-8') : '';
|
||||
|
||||
props.jsx = tsToJs(code);
|
||||
|
||||
if (md) {
|
||||
// extract description & css style from md file
|
||||
const blocks: Record<string, string> = {};
|
||||
|
||||
const lines = md.split('\n');
|
||||
|
||||
let blockName = '';
|
||||
let cacheList: string[] = [];
|
||||
|
||||
// Get block name
|
||||
const getBlockName = (text: string) => {
|
||||
if (text.startsWith('## ')) {
|
||||
return text.replace('## ', '').trim();
|
||||
}
|
||||
|
||||
if (text.startsWith('```css') || text.startsWith('<style>')) {
|
||||
return 'style';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Fill block content
|
||||
const fillBlock = (name: string, lineList: string[]) => {
|
||||
if (lineList.length) {
|
||||
let fullText: string;
|
||||
|
||||
if (name === 'style') {
|
||||
fullText = lineList
|
||||
.join('\n')
|
||||
.replace(/<\/?style>/g, '')
|
||||
.replace(/```(\s*css)/g, '');
|
||||
} else {
|
||||
fullText = lineList.slice(1).join('\n');
|
||||
}
|
||||
|
||||
blocks[name] = fullText;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Mark as new block
|
||||
const nextBlockName = getBlockName(line);
|
||||
if (nextBlockName) {
|
||||
fillBlock(blockName, cacheList);
|
||||
|
||||
// Next Block
|
||||
blockName = nextBlockName;
|
||||
cacheList = [line];
|
||||
} else {
|
||||
cacheList.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Last block
|
||||
fillBlock(blockName, cacheList);
|
||||
|
||||
props.description = blocks[locale];
|
||||
props.style = blocks.style;
|
||||
}
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
export default function techStackPlugin(api: IApi) {
|
||||
api.registerTechStack(() => new AntdReactTechStack());
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { RightOutlined, YuqueOutlined, ZhihuOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Divider } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
@@ -10,7 +10,7 @@ import JuejinIcon from '../../../theme/icons/JuejinIcon';
|
||||
const ANTD_IMG_URL =
|
||||
'https://picx.zhimg.com/v2-3b2bca09c2771e7a82a81562e806be4d.jpg?source=d16d100b';
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
card: css`
|
||||
width: 100%;
|
||||
margin: calc(${cssVar.marginMD} * 2) 0;
|
||||
@@ -113,19 +113,17 @@ interface Props {
|
||||
const ColumnCard: React.FC<Props> = ({ zhihuLink, yuqueLink, juejinLink }) => {
|
||||
const [locale] = useLocale(locales);
|
||||
const {
|
||||
styles: {
|
||||
card,
|
||||
bigTitle,
|
||||
cardBody,
|
||||
leftCard,
|
||||
title,
|
||||
subTitle,
|
||||
logo,
|
||||
arrowIcon,
|
||||
zlBtn,
|
||||
discussLogo,
|
||||
},
|
||||
} = useStyle();
|
||||
card,
|
||||
bigTitle,
|
||||
cardBody,
|
||||
leftCard,
|
||||
title,
|
||||
subTitle,
|
||||
logo,
|
||||
arrowIcon,
|
||||
zlBtn,
|
||||
discussLogo,
|
||||
} = styles;
|
||||
if (!zhihuLink && !yuqueLink && !juejinLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import ContributorsList from '@qixian.cs/github-contributors-list';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
import { useIntl } from 'dumi';
|
||||
|
||||
import SiteContext from '../SiteContext';
|
||||
import ContributorAvatar from './ContributorAvatar';
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => ({
|
||||
const styles = createStaticStyles(({ cssVar, css }) => ({
|
||||
listMobile: css`
|
||||
margin: 1em 0 !important;
|
||||
`,
|
||||
@@ -48,7 +48,6 @@ const blockList = [
|
||||
|
||||
const Contributors: React.FC<ContributorsProps> = ({ filename }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { styles } = useStyle();
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
|
||||
if (!filename) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { removeCSS, updateCSS } from '@rc-component/util/lib/Dom/dynamicCSS';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
|
||||
@@ -20,7 +20,7 @@ const locales = {
|
||||
},
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ css, cssVar }) => ({
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
position: fixed;
|
||||
inset-inline-start: 0;
|
||||
@@ -80,8 +80,6 @@ const InfoNewVersion: React.FC = () => {
|
||||
removeCSS(whereCls);
|
||||
}, []);
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
if (supportWhere) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { omit } from '@rc-component/util';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
export interface SwitchBtnProps {
|
||||
@@ -18,7 +18,7 @@ export interface SwitchBtnProps {
|
||||
|
||||
const BASE_SIZE = '1.2em';
|
||||
|
||||
const useStyle = createStyles(({ cssVar, css }) => {
|
||||
const styles = createStaticStyles(({ cssVar, css }) => {
|
||||
return {
|
||||
btn: css`
|
||||
width: ${cssVar.controlHeight};
|
||||
@@ -65,9 +65,7 @@ const useStyle = createStyles(({ cssVar, css }) => {
|
||||
const SwitchBtn: React.FC<SwitchBtnProps> = (props) => {
|
||||
const { label1, label2, tooltip1, tooltip2, value, pure, onClick, ...rest } = props;
|
||||
|
||||
const {
|
||||
styles: { btn, innerDiv, labelStyle, label1Style, label2Style },
|
||||
} = useStyle();
|
||||
const { btn, innerDiv, labelStyle, label1Style, label2Style } = styles;
|
||||
|
||||
const node = (
|
||||
<Button
|
||||
|
||||
@@ -2,11 +2,14 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { GithubOutlined, MenuOutlined } from '@ant-design/icons';
|
||||
import { Alert, Button, Col, ConfigProvider, Popover, Row, Select, Tooltip } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import { clsx } from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { useLocation, useSiteData } from 'dumi';
|
||||
import DumiSearchBar from 'dumi/theme-default/slots/SearchBar';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import versionsFile from '../../../../public/versions.json';
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import useLocalStorage from '../../../hooks/useLocalStorage';
|
||||
import { getBannerData } from '../../../pages/index/components/util';
|
||||
@@ -14,7 +17,6 @@ import ThemeSwitch from '../../common/ThemeSwitch';
|
||||
import DirectionIcon from '../../icons/DirectionIcon';
|
||||
import { ANT_DESIGN_NOT_SHOW_BANNER } from '../../layouts/GlobalLayout';
|
||||
import * as utils from '../../utils';
|
||||
import { getThemeConfig } from '../../utils';
|
||||
import SiteContext from '../SiteContext';
|
||||
import type { SharedProps } from './interface';
|
||||
import Logo from './Logo';
|
||||
@@ -132,7 +134,8 @@ const useStyle = createStyles(({ cssVar, token, css }) => {
|
||||
}
|
||||
`,
|
||||
versionSelect: css`
|
||||
min-width: 90px;
|
||||
width: 112px;
|
||||
min-width: 112px; // 这个宽度需要和 Empty 状态的宽度保持一致
|
||||
.rc-virtual-list {
|
||||
.rc-virtual-list-holder {
|
||||
scrollbar-width: thin;
|
||||
@@ -149,19 +152,56 @@ interface HeaderState {
|
||||
searching: boolean;
|
||||
}
|
||||
|
||||
interface VersionItem {
|
||||
version: string;
|
||||
url: string;
|
||||
chineseMirrorUrl?: string;
|
||||
}
|
||||
|
||||
const fetcher = (...args: Parameters<typeof fetch>) => {
|
||||
return fetch(...args).then((res) => res.json());
|
||||
};
|
||||
|
||||
// ================================= Header =================================
|
||||
const Header: React.FC = () => {
|
||||
const [, lang] = useLocale();
|
||||
|
||||
const { pkg } = useSiteData();
|
||||
|
||||
const themeConfig = getThemeConfig();
|
||||
const isChineseMirror =
|
||||
typeof window !== 'undefined' && typeof window.location !== 'undefined'
|
||||
? window.location.hostname.includes('.antgroup.com')
|
||||
: false;
|
||||
|
||||
const { data: versions = [], isLoading } = useSWR<VersionItem[]>(
|
||||
process.env.NODE_ENV === 'production' && typeof window !== 'undefined'
|
||||
? `${window.location.origin}/versions.json`
|
||||
: null,
|
||||
fetcher,
|
||||
{
|
||||
fallbackData: versionsFile,
|
||||
errorRetryCount: 3,
|
||||
},
|
||||
);
|
||||
|
||||
const versionOptions = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return [];
|
||||
}
|
||||
return versions.map<DefaultOptionType>((item) => {
|
||||
const isMatch = item.version.startsWith(pkg.version[0]);
|
||||
const label = isMatch ? pkg.version : item.version;
|
||||
const value = isChineseMirror && item.chineseMirrorUrl ? item.chineseMirrorUrl : item.url;
|
||||
return { value, label };
|
||||
});
|
||||
}, [versions, isLoading, pkg.version, isChineseMirror]);
|
||||
|
||||
const [headerState, setHeaderState] = useState<HeaderState>({
|
||||
menuVisible: false,
|
||||
windowWidth: 1400,
|
||||
searching: false,
|
||||
});
|
||||
|
||||
const { direction, isMobile, bannerVisible, updateSiteConfig } = React.use(SiteContext);
|
||||
const pingTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const location = useLocation();
|
||||
@@ -258,14 +298,6 @@ const Header: React.FC = () => {
|
||||
);
|
||||
|
||||
const { menuVisible, windowWidth, searching } = headerState;
|
||||
const docVersions: Record<string, string> = {
|
||||
[pkg.version]: pkg.version,
|
||||
...themeConfig?.docVersions,
|
||||
};
|
||||
const versionOptions = Object.keys(docVersions).map((version) => ({
|
||||
value: docVersions[version],
|
||||
label: version,
|
||||
}));
|
||||
|
||||
const isHome = ['', 'index', 'index-cn'].includes(pathname);
|
||||
const isZhCN = lang === 'cn';
|
||||
@@ -308,6 +340,7 @@ const Header: React.FC = () => {
|
||||
key="version"
|
||||
size="small"
|
||||
variant="filled"
|
||||
loading={isLoading}
|
||||
className={styles.versionSelect}
|
||||
defaultValue={pkg.version}
|
||||
onChange={handleVersionChange}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
const chineseMirror =
|
||||
typeof location !== 'undefined' && location.hostname.includes('.antgroup.com');
|
||||
|
||||
export default {
|
||||
categoryOrder: {
|
||||
'Ant Design': 0,
|
||||
@@ -45,15 +42,4 @@ export default {
|
||||
模板文档: 3,
|
||||
'Template Document': 3,
|
||||
},
|
||||
docVersions: {
|
||||
'5.x': chineseMirror ? 'https://5x-ant-design.antgroup.com' : 'https://5x.ant.design',
|
||||
'4.x': chineseMirror ? 'https://4x-ant-design.antgroup.com' : 'https://4x.ant.design',
|
||||
'3.x': 'https://3x.ant.design',
|
||||
'2.x': 'https://2x.ant.design',
|
||||
'1.x': 'https://1x.ant.design',
|
||||
'0.12.x': 'https://012x.ant.design',
|
||||
'0.11.x': 'https://011x.ant.design',
|
||||
'0.10.x': 'https://010x.ant.design',
|
||||
'0.9.x': 'https://09x.ant.design',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import flattenDeep from 'lodash/flattenDeep';
|
||||
import semver from 'semver';
|
||||
|
||||
import deprecatedVersions from '../../../BUG_VERSIONS.json';
|
||||
import themeConfig from '../themeConfig';
|
||||
|
||||
interface Meta {
|
||||
skip?: boolean;
|
||||
@@ -206,4 +205,12 @@ export function matchDeprecated(v: string): MatchDeprecatedResult {
|
||||
};
|
||||
}
|
||||
|
||||
export const getThemeConfig = () => themeConfig;
|
||||
/**
|
||||
* Determine if a hostname is an official domain.
|
||||
* antd creates a temporary preview site for each PR for convenient preview and testing.
|
||||
* Usually on platforms like surge.sh or Cloudflare Pages.
|
||||
*/
|
||||
export function isOfficialHost(hostname: string) {
|
||||
const officialHostnames = ['ant.design', 'antgroup.com'];
|
||||
return officialHostnames.some((official) => hostname.includes(official));
|
||||
}
|
||||
|
||||
23
.dumirc.ts
23
.dumirc.ts
@@ -160,18 +160,27 @@ export default defineConfig({
|
||||
}
|
||||
|
||||
// 首页无视链接里面的语言设置 https://github.com/ant-design/ant-design/issues/4552
|
||||
if (pathname === '/' || pathname === '/index-cn') {
|
||||
const lang =
|
||||
(window.localStorage && localStorage.getItem('locale')) ||
|
||||
((navigator.language || navigator.browserLanguage).toLowerCase() === 'zh-cn'
|
||||
const normalizedPathname = pathname || '/';
|
||||
if (normalizedPathname === '/' || normalizedPathname === '/index-cn') {
|
||||
let lang;
|
||||
if (window.localStorage) {
|
||||
const antLocale = localStorage.getItem('ANT_LOCAL_TYPE_KEY');
|
||||
// 尝试解析 JSON,因为可能是被序列化后存储的 "en-US" / en-US https://github.com/ant-design/ant-design/issues/56606
|
||||
try {
|
||||
lang = antLocale ? JSON.parse(antLocale) : localStorage.getItem('locale');
|
||||
} catch (e) {
|
||||
lang = antLocale ? antLocale : localStorage.getItem('locale');
|
||||
}
|
||||
}
|
||||
lang = lang || ((navigator.language || navigator.browserLanguage).toLowerCase() === 'zh-cn'
|
||||
? 'zh-CN'
|
||||
: 'en-US');
|
||||
// safari is 'zh-cn', while other browser is 'zh-CN';
|
||||
if ((lang === 'zh-CN') !== isZhCN(pathname)) {
|
||||
location.pathname = getLocalizedPathname(pathname, lang === 'zh-CN');
|
||||
if ((lang === 'zh-CN') !== isZhCN(normalizedPathname)) {
|
||||
location.pathname = getLocalizedPathname(normalizedPathname, lang === 'zh-CN');
|
||||
}
|
||||
}
|
||||
document.documentElement.className += isZhCN(pathname) ? 'zh-cn' : 'en-us';
|
||||
document.documentElement.className += isZhCN(normalizedPathname) ? 'zh-cn' : 'en-us';
|
||||
})();
|
||||
`,
|
||||
],
|
||||
|
||||
2
.github/workflows/discussion-open-check.yml
vendored
2
.github/workflows/discussion-open-check.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
discussion-create:
|
||||
permissions:
|
||||
contents: read # for visiky/dingtalk-release-notify to get latest release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: send to dingtalk
|
||||
uses: visiky/dingtalk-release-notify@main
|
||||
|
||||
2
.github/workflows/issue-check-inactive.yml
vendored
2
.github/workflows/issue-check-inactive.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write # for actions-cool/issues-helper to update issues
|
||||
pull-requests: write # for actions-cool/issues-helper to update PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: check-inactive
|
||||
uses: actions-cool/issues-helper@v3
|
||||
|
||||
2
.github/workflows/issue-close-require.yml
vendored
2
.github/workflows/issue-close-require.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write # for actions-cool/issues-helper to update issues
|
||||
pull-requests: write # for actions-cool/issues-helper to update PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: need reproduce
|
||||
uses: actions-cool/issues-helper@v3
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
reminder_job:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Send reminders for inactive issues
|
||||
uses: actions/github-script@v8
|
||||
|
||||
2
.github/workflows/issue-labeled.yml
vendored
2
.github/workflows/issue-labeled.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write # for actions-cool/issues-helper to update issues
|
||||
pull-requests: write # for actions-cool/issues-helper to update PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: help wanted
|
||||
if: github.event.label.name == 'help wanted'
|
||||
|
||||
2
.github/workflows/issue-open-check.yml
vendored
2
.github/workflows/issue-open-check.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
contents: read # for visiky/dingtalk-release-notify to get latest release
|
||||
issues: write # for actions-cool/issues-helper to update issues
|
||||
pull-requests: write # for actions-cool/issues-helper to update PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions-cool/check-user-permission@v2
|
||||
id: checkUser
|
||||
|
||||
2
.github/workflows/issue-remove-inactive.yml
vendored
2
.github/workflows/issue-remove-inactive.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write # for actions-cool/issues-helper to update issues
|
||||
pull-requests: write # for actions-cool/issues-helper to update PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: remove inactive
|
||||
if: github.event.issue.state == 'open' && github.actor == github.event.issue.user.login
|
||||
|
||||
2
.github/workflows/issue-schedule.yml
vendored
2
.github/workflows/issue-schedule.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
send-message:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Send Unconfirmed Issues to DingTalk
|
||||
uses: actions/github-script@v8
|
||||
|
||||
2
.github/workflows/mock-project-build.yml
vendored
2
.github/workflows/mock-project-build.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
jobs:
|
||||
pr-check-ci:
|
||||
if: github.repository == 'ant-design/ant-design'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
name: Build Project
|
||||
steps:
|
||||
- name: checkout
|
||||
|
||||
2
.github/workflows/pkg.pr.new.yml
vendored
2
.github/workflows/pkg.pr.new.yml
vendored
@@ -3,7 +3,7 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
2
.github/workflows/pr-auto-merge.yml
vendored
2
.github/workflows/pr-auto-merge.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
contents: write # for actions-cool/check-pr-ci to merge PRs
|
||||
issues: write # for actions-cool/check-pr-ci to update issues
|
||||
pull-requests: write # for actions-cool/check-pr-ci to update PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions-cool/check-pr-ci@v1
|
||||
with:
|
||||
|
||||
4
.github/workflows/pr-check-merge.yml
vendored
4
.github/workflows/pr-check-merge.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
||||
permissions:
|
||||
issues: write # for actions-cool/issues-helper to update issues
|
||||
pull-requests: write # for actions-cool/issues-helper to update PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: (github.event.pull_request.head.ref == 'next' || github.event.pull_request.head.ref == 'feature' || github.event.pull_request.head.ref == 'master') && github.event.pull_request.head.user.login == 'ant-design'
|
||||
steps:
|
||||
- uses: actions-cool/issues-helper@d1d51fccf39469b5458203b1369060db0ff0c0db
|
||||
- uses: actions-cool/issues-helper@e2ff99831a4f13625d35064e2b3dfe65c07a0396
|
||||
with:
|
||||
actions: create-comment
|
||||
issue-number: ${{ github.event.number }}
|
||||
|
||||
2
.github/workflows/pr-contributor-welcome.yml
vendored
2
.github/workflows/pr-contributor-welcome.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
issues: write # for actions-cool/maintain-one-comment to modify or create issue comments
|
||||
pull-requests: write # for actions-cool/maintain-one-comment to modify or create PR comments
|
||||
if: github.event.pull_request.merged == true && github.repository == 'ant-design/ant-design'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: get commit count
|
||||
id: get_commit_count
|
||||
|
||||
4
.github/workflows/pr-open-check.yml
vendored
4
.github/workflows/pr-open-check.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write # for actions-cool/pr-welcome to create, update & react on issues
|
||||
pull-requests: write # for actions-cool/pr-welcome to request reviewer
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions-cool/pr-welcome@4bd317d60ef3b40a3ccda39c22f66c3358010f92
|
||||
with:
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
check-changelog:
|
||||
permissions:
|
||||
pull-requests: write # for actions-cool/pr-check-fill to create or update PR comments
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: check fill
|
||||
uses: actions-cool/pr-check-fill@35194e32fd717c88c4fde15fbde9005933c2d452
|
||||
|
||||
2
.github/workflows/pr-open-notify.yml
vendored
2
.github/workflows/pr-open-notify.yml
vendored
@@ -9,7 +9,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
send-to-dingtalk:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: send to dingtalk
|
||||
uses: visiky/dingtalk-release-notify@64fcb0373782b6c2f6d9b9ea3c68af80ca189585
|
||||
|
||||
8
.github/workflows/preview-deploy.yml
vendored
8
.github/workflows/preview-deploy.yml
vendored
@@ -14,7 +14,7 @@ permissions:
|
||||
jobs:
|
||||
upstream-workflow-summary:
|
||||
name: upstream workflow summary
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
outputs:
|
||||
jobs: ${{ steps.prep-summary.outputs.result }}
|
||||
@@ -57,13 +57,13 @@ jobs:
|
||||
actions: read # for dawidd6/action-download-artifact to query and download artifacts
|
||||
issues: write # for actions-cool/maintain-one-comment to modify or create issue comments
|
||||
pull-requests: write # for actions-cool/maintain-one-comment to modify or create PR comments
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
needs: upstream-workflow-summary
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
steps:
|
||||
# We need get PR id first
|
||||
- name: download pr artifact
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
uses: dawidd6/action-download-artifact@v14
|
||||
with:
|
||||
workflow: ${{ github.event.workflow_run.workflow_id }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
# Download site artifact
|
||||
- name: download site artifact
|
||||
if: ${{ fromJSON(needs.upstream-workflow-summary.outputs.build-success) }}
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
uses: dawidd6/action-download-artifact@v14
|
||||
with:
|
||||
workflow: ${{ github.event.workflow_run.workflow_id }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
2
.github/workflows/preview-start.yml
vendored
2
.github/workflows/preview-start.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
issues: write # for actions-cool/maintain-one-comment to modify or create issue comments
|
||||
pull-requests: write # for actions-cool/maintain-one-comment to modify or create PR comments
|
||||
name: start preview info
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: update status comment
|
||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b
|
||||
|
||||
2
.github/workflows/release-dingtalk.yml
vendored
2
.github/workflows/release-dingtalk.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write # for actions-cool/release-helper to create releases
|
||||
if: github.event.ref_type == 'tag'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Send to Ant Design DingGroup
|
||||
uses: actions-cool/release-helper@v2
|
||||
|
||||
2
.github/workflows/release-x.yml
vendored
2
.github/workflows/release-x.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
tweet:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: ${{ github.event.ref_type == 'tag' && !contains(github.event.ref, 'alpha') }}
|
||||
steps:
|
||||
- name: Tweet
|
||||
|
||||
4
.github/workflows/site-deploy.yml
vendored
4
.github/workflows/site-deploy.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
run: echo "VERSION=$(echo ${{ github.ref_name }} | sed 's/\./-/g')" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy-to-pages:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
needs: build-site
|
||||
steps:
|
||||
- uses: utooland/setup-utoo@v1
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
|
||||
# https://github.com/ant-design/ant-design/pull/49213/files#r1625446496
|
||||
upload-to-release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
# 仅在 tag 的时候工作,因为我们要将内容发布到以 tag 为版本号的 release 里
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: build-site
|
||||
|
||||
2
.github/workflows/size-limit.yml
vendored
2
.github/workflows/size-limit.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user