Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: doc icon #64

Merged
merged 1 commit into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
6 changes: 6 additions & 0 deletions .dumi/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import { ConfigProvider } from 'antd';

export function rootContainer(container: React.ReactNode): React.ReactNode {
return <ConfigProvider>{container}</ConfigProvider>;
}
48 changes: 48 additions & 0 deletions .dumi/global.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#root {
.dumi-default-header-left {
width: 300px;
}
.dumi-default-header-right {
justify-content: end;
}
.dumi-default-navbar {
margin-right: 24px;
}

.dumi-default-hero-title {
font-size: 80px;
}

.dumi-default-content-footer {
line-height: 1.2;
}

.home {
main {
max-width: none;
padding: 0;
}
.dumi-default-header-content {
max-width: none;
}
}
}

:not([data-prefers-color='dark']) {
.home {
.dumi-default-header {
background-color: #fff;
}
.dumi-default-content {
background-color: #fff;
}
}
}

[data-prefers-color='dark'] {
.home {
.dumi-default-content {
background-color: #050709;
}
}
}
56 changes: 56 additions & 0 deletions .dumi/theme/SiteThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useEffect } from 'react';
import { AELFDProvider } from 'aelf-design';
import { theme as antdTheme, type ThemeConfig } from 'antd';
import { ThemeProvider, useTheme, type ThemeProviderProps } from 'antd-style';

interface NewToken {
bannerHeight: number;
headerHeight: number;
anchorTop: number;
}

// 通过给 antd-style 扩展 CustomToken 对象类型定义,可以为 useTheme 中增加相应的 token 对象
declare module 'antd-style' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CustomToken extends NewToken {}
}

const headerHeight = 64;
const bannerHeight = 38;

const SiteThemeProvider: React.FC<ThemeProviderProps<any>> = ({
children,
appearance = 'light',
...rest
}) => {
return (
<AELFDProvider
prefixCls="aelf-design-doc"
appearance={appearance}
customToken={{
// customButton: {
// borderRadiusDefault: '20px',
// borderRadiusLarge: '30px'
// },
customAddress: {
primaryLinkColor: appearance == 'dark' ? '#c713af' : '#53dd13',
primaryIconColor: appearance == 'dark' ? '#ea1818' : '#7f7777',
addressHoverColor: appearance == 'dark' ? '#38b117' : '#149434',
addressActiveColor: appearance == 'dark' ? '#0756BC' : '#0460D9',
},
}}
theme={{
token: {
colorPrimary: appearance == 'dark' ? '#1370DD' : '#764DF1',
colorPrimaryHover: appearance == 'dark' ? '#3689DD' : '#7F58F5',
colorPrimaryActive: appearance == 'dark' ? '#0756BC' : '#6F45EF',
colorTextDisabled: appearance == 'dark' ? '#96C4FB' : '#ACD2FF',
},
}}
>
{children}
</AELFDProvider>
);
};

export default SiteThemeProvider;
61 changes: 61 additions & 0 deletions .dumi/theme/builtins/IconSearch/Category.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as React from 'react';
import { message } from 'antd';
import { useIntl } from 'dumi';

import CopyableIcon from './CopyableIcon';
import { type CategoriesKeys } from './fields';
import { type ThemeType } from './IconSearch';

interface CategoryProps {
title: CategoriesKeys;
icons: string[];
theme: ThemeType;
newIcons: string[];
}

const Category: React.FC<CategoryProps> = (props) => {
const { icons, title, newIcons, theme } = props;
const intl = useIntl();
const [justCopied, setJustCopied] = React.useState<string | null>(null);
const copyId = React.useRef<ReturnType<typeof setTimeout> | null>(null);

const onCopied = React.useCallback((type: string, text: string) => {
message.success(
<span>
<code className="copied-code">{text}</code> copied 🎉
</span>,
);
setJustCopied(type);
copyId.current = setTimeout(() => {
setJustCopied(null);
}, 200);
}, []);
React.useEffect(
() => () => {
if (copyId.current) {
clearTimeout(copyId.current);
}
},
[],
);

return (
<div>
<h3>{intl.formatMessage({ id: `app.docs.components.icon.category.${title}` })}</h3>
<ul className="anticons-list">
{icons.map((name) => (
<CopyableIcon
key={name}
name={name}
theme={theme}
isNew={newIcons.includes(name)}
justCopied={justCopied}
onCopied={onCopied}
/>
))}
</ul>
</div>
);
};

export default Category;
56 changes: 56 additions & 0 deletions .dumi/theme/builtins/IconSearch/CopyableIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react';
import * as AntdWeb3Icons from '@aelf-design/internal-icons';
import { Badge, message } from 'antd';
import classNames from 'classnames';
import CopyToClipboard from 'react-copy-to-clipboard';

import type { ThemeType } from './IconSearch';

const allIcons: {
[key: string]: any;
} = AntdWeb3Icons;

export interface CopyableIconProps {
name: string;
isNew: boolean;
theme: ThemeType;
justCopied: string | null;
onCopied: (type: string, text: string) => any;
}

const CopyableIcon: React.FC<CopyableIconProps> = ({
name,
isNew,
justCopied,
onCopied,
theme,
}) => {
const className = classNames({
copied: justCopied === name,
[theme]: !!theme,

// 一些白色图标需要添加背景色
isWhite: name.includes('White'),
});

const onCopy = (text: string, result: boolean) => {
if (result) {
onCopied(name, text);
} else {
message.error('Copy icon name failed, please try again.');
}
};

return (
<CopyToClipboard text={`<${name} />`} onCopy={onCopy}>
<li className={className}>
{React.createElement(allIcons[name])}
<span className="anticon-class">
<Badge dot={isNew}>{name}</Badge>
</span>
</li>
</CopyToClipboard>
);
};

export default CopyableIcon;
143 changes: 143 additions & 0 deletions .dumi/theme/builtins/IconSearch/IconSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { CSSProperties, useCallback, useMemo, useState } from 'react';
import * as AntdWeb3Icons from '@aelf-design/internal-icons';
import AntdIcon from '@ant-design/icons';
import { Affix, Empty, Grid, Input, Segmented, type SegmentedProps } from 'antd';
import { createStyles, useTheme } from 'antd-style';
import { useIntl } from 'dumi';
import debounce from 'lodash/debounce';

import Category from './Category';
import { categories, CategoriesKeys } from './fields';
import { CircleFilledIcon, FilledIcon } from './themeIcons';

export enum ThemeType {
Filled = 'Filled',
CircleFilled = 'CircleFilled',
Colorful = 'Colorful',
CircleColorful = 'CircleColorful',
}

const allIcons: Record<string, any> = AntdWeb3Icons;

const useStyle = createStyles(({ css }) => ({
iconSearchAffix: css`
display: flex;
transition: all 0.3s;
justify-content: space-between;
`,
}));

const options = (
formatMessage: (values: Record<string, string>) => React.ReactNode,
onlyIcon?: boolean,
): SegmentedProps['options'] => [
{
value: ThemeType.CircleFilled,
icon: <AntdIcon component={CircleFilledIcon} />,
label: !onlyIcon && formatMessage({ id: 'app.docs.components.icon.circle-filled' }),
},
{
value: ThemeType.Filled,
icon: <AntdIcon component={FilledIcon} />,
label: !onlyIcon && formatMessage({ id: 'app.docs.components.icon.filled' }),
},
];

interface IconSearchState {
theme: ThemeType;
searchKey: string;
}

const IconSearch: React.FC = () => {
const intl = useIntl();
const token = useTheme();
const { md } = Grid.useBreakpoint();
const { styles } = useStyle();
const [displayState, setDisplayState] = useState<IconSearchState>({
searchKey: '',
theme: ThemeType.CircleColorful,
});

const newIconNames: string[] = [];

const handleSearchIcon = debounce((e: React.ChangeEvent<HTMLInputElement>) => {
setDisplayState((prevState) => ({ ...prevState, searchKey: e.target.value }));
}, 300);

const handleChangeTheme = useCallback((theme: any) => {
setDisplayState((prevState) => ({ ...prevState, theme }));
}, []);

const renderCategories = useMemo<React.ReactNode | React.ReactNode[]>(() => {
const { searchKey = '', theme } = displayState;

const categoriesResult = Object.keys(categories)
.map((key) => {
let iconList = categories[key as CategoriesKeys];
if (searchKey) {
const matchKey = searchKey
.replace(new RegExp(`^<([a-zA-Z]*)\\s/>$`, 'gi'), (_, name) => name)
.replace(/(Colorful|Filled|CircleFilled)$/gi, '')
.toLowerCase();
iconList = iconList.filter((iconName) => iconName.toLowerCase().includes(matchKey));
}

return {
category: key,
icons: iconList
.map((iconName) => iconName + theme)
.filter((iconName) => allIcons[iconName]),
};
})
.filter(({ icons }) => !!icons.length)
.map(({ category, icons }) => (
<Category
key={category}
title={category as CategoriesKeys}
theme={theme}
icons={icons}
newIcons={newIconNames}
/>
));

return categoriesResult.length ? categoriesResult : <Empty style={{ margin: '2em 0' }} />;
}, [displayState.searchKey, displayState.theme]);

const [searchBarAffixed, setSearchBarAffixed] = useState<boolean | undefined>(false);

const { borderRadius, colorBgContainer, anchorTop } = token;

const affixedStyle: CSSProperties = {
boxShadow: 'rgba(50, 50, 93, 0.25) 0 6px 12px -2px, rgba(0, 0, 0, 0.3) 0 3px 7px -3px',
padding: 8,
margin: -8,
borderRadius,
backgroundColor: colorBgContainer,
};

return (
<div className="markdown">
<Affix offsetTop={anchorTop} onChange={setSearchBarAffixed}>
<div className={styles.iconSearchAffix} style={searchBarAffixed ? affixedStyle : {}}>
<Segmented
size="large"
value={displayState.theme}
options={options(intl.formatMessage, !md)}
onChange={handleChangeTheme}
/>
<Input.Search
placeholder={intl.formatMessage({ id: 'app.docs.components.icon.search.placeholder' })}
style={{ flex: 1, marginInlineStart: 16 }}
allowClear
autoFocus
size="large"
onChange={handleSearchIcon}
/>
</div>
</Affix>
{renderCategories}
</div>
);
};

export default IconSearch;
Loading
Loading