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

feat: add Theme Config Editor #39621

Merged
merged 4 commits into from Dec 27, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions .dumi/pages/theme-editor/components/JSONEditor.tsx
@@ -0,0 +1,33 @@
import { JSONEditor as Editor, Mode, type JSONEditorPropsOptional } from 'vanilla-jsoneditor';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vanilla-jsoneditor 可以加到 https://ant.design/docs/react/recommendation-cn 里来

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vanilla-jsoneditor 可以加到 https://ant.design/docs/react/recommendation-cn 里来

收到🫡

import React from 'react';

const JSONEditor = (props: JSONEditorPropsOptional) => {
const refContainer = React.useRef(null);
const refEditor = React.useRef(null);

React.useEffect(() => {
refEditor.current = new Editor({
target: refContainer.current,
props: {
mode: Mode.text,
},
});

return () => {
if (refEditor.current) {
refEditor.current.destroy();
refEditor.current = null;
}
};
}, []);

React.useEffect(() => {
if (refEditor.current) {
refEditor.current.updateProps(props);
}
}, [props]);

return <div className="vanilla-jsoneditor-react" ref={refContainer} />;
};

export default JSONEditor;
4 changes: 4 additions & 0 deletions .dumi/pages/theme-editor/components/utils.tsx
@@ -0,0 +1,4 @@
/* eslint-disable import/prefer-default-export */
export function isObject(target: any) {
return Object.prototype.toString.call(target) === '[object Object]';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

这个好像没办法作为 lodash 的平替吧,lodash 还判断了数组什么的

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里我用错了,只想要纯Object,所以这个就够了,不需要_.isObject

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK,那就更不需要 lodash 了,因为 antd 中用到 lodash 的地方不多,所以后面计划把这个库干掉,体积实在太大了

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK,那就更不需要 lodash 了,因为 antd 中用到 lodash 的地方不多,所以后面计划把这个库干掉,体积实在太大了

哈哈哈,看到你们的pr了

}
131 changes: 84 additions & 47 deletions .dumi/pages/theme-editor/index.tsx
@@ -1,28 +1,36 @@
import React, { useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { enUS, zhCN, ThemeEditor } from 'antd-token-previewer';
import { Button, ConfigProvider, message, Modal, Typography } from 'antd';
import type { ThemeConfig } from 'antd/es/config-provider/context';
import { Helmet } from 'dumi';
import { css } from '@emotion/react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { CopyOutlined } from '@ant-design/icons';
import { EditOutlined } from '@ant-design/icons';
import type { JSONContent, TextContent } from 'vanilla-jsoneditor';
import useLocale from '../../hooks/useLocale';
import JSONEditor from './components/JSONEditor';
import { isObject } from './components/utils';

const locales = {
cn: {
title: '主题编辑器',
save: '保存',
reset: '重置',
export: '导出',
exportDesc: '将下面的 JSON 对象复制到 ConfigProvider 的 theme 属性中即可。',
edit: '代码',
editModelTitle: '编辑主题配置',
editTitle: '在下方编辑你的主题 JSON 即可',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

将 JSON 复制到 ConfigProvider 的 theme 属性中套用主题。

editJsonContentTypeError: '主题 JSON 格式错误',
editSuccessfully: '编辑成功',
saveSuccessfully: '保存成功',
},
en: {
title: 'Theme Editor',
save: 'Save',
reset: 'Reset',
export: 'Export',
exportDesc: 'Copy the following JSON object to the theme prop of ConfigProvider.',
edit: 'Code',
editModelTitle: 'edit Theme Config',
editTitle: 'Edit your theme JSON below',
editJsonContentTypeError: 'The theme of the JSON format is incorrect',
editSuccessfully: 'Edited successfully',
saveSuccessfully: 'Saved successfully',
},
};
Expand All @@ -42,83 +50,112 @@ const ANT_DESIGN_V5_THEME_EDITOR_THEME = 'ant-design-v5-theme-editor-theme';

const CustomTheme = () => {
const [messageApi, contextHolder] = message.useMessage();
const [modalApi, modalContextHolder] = Modal.useModal();
const [locale, lang] = useLocale(locales);

const [theme, setTheme] = React.useState<ThemeConfig>({});

const [editModelOpen, setEditModelOpen] = useState<boolean>(false);
const [editThemeFormatRight, setEditThemeFormatRight] = useState<boolean>(true);
const [themeConfigContent, setThemeConfigContent] = useState<JSONContent & TextContent>({
text: '{}',
json: undefined,
});

useEffect(() => {
const storedConfig = localStorage.getItem(ANT_DESIGN_V5_THEME_EDITOR_THEME);
if (storedConfig) {
setTheme(() => JSON.parse(storedConfig));
}
}, []);

useEffect(() => {
if (editModelOpen === true) return;
setThemeConfigContent({
json: theme as any,
text: undefined,
});
}, [theme, editModelOpen]);

const styles = useStyle();

const handleSave = () => {
localStorage.setItem(ANT_DESIGN_V5_THEME_EDITOR_THEME, JSON.stringify(theme));
messageApi.success(locale.saveSuccessfully);
};

const onCopy = (text: string, result: boolean) => {
if (result) {
messageApi.success('Copy theme config successfully!');
} else {
messageApi.error('Copy failed, please try again.');
}
const handleReset = () => {
setTheme({});
};

const handleOutput = () => {
modalApi.info({
title: locale.export,
width: 600,
content: (
<div>
<div style={{ color: 'rgba(0,0,0,0.65)' }}>{locale.exportDesc}</div>
<pre
style={{
padding: 12,
background: '#f5f5f5',
borderRadius: 4,
marginTop: 12,
position: 'relative',
}}
>
<CopyToClipboard text={JSON.stringify(theme, null, 2)} onCopy={onCopy}>
<Button
type="text"
icon={<CopyOutlined />}
style={{ position: 'absolute', right: 8, top: 8 }}
/>
</CopyToClipboard>
{JSON.stringify(theme, null, 2)}
</pre>
</div>
),
});
const handleEditConfig = () => {
setEditModelOpen(true);
};

const handleReset = () => {
setTheme({});
const editModelClose = useCallback(() => {
setEditModelOpen(false);
}, [themeConfigContent]);

const handleEditConfigChange = (newcontent, preContent, status) => {
setThemeConfigContent(newcontent);
if (
Array.isArray(status.contentErrors.validationErrors) &&
status.contentErrors.validationErrors.length === 0
) {
setEditThemeFormatRight(true);
} else {
setEditThemeFormatRight(false);
}
};

const editSave = useCallback(() => {
if (!editThemeFormatRight) {
message.error(locale.editJsonContentTypeError);
return;
}
const themeConfig = themeConfigContent.text
? JSON.parse(themeConfigContent.text)
: themeConfigContent.json;
if (!isObject(themeConfig)) {
message.error(locale.editJsonContentTypeError);
return;
}
setTheme(themeConfig);
editModelClose();
messageApi.success(locale.editSuccessfully);
}, [themeConfigContent]);

return (
<div>
<Helmet>
<title>{`${locale.title} - Ant Design`}</title>
<meta property="og:title" content={`${locale.title} - Ant Design`} />
</Helmet>
{contextHolder}
{modalContextHolder}
<ConfigProvider theme={{ inherit: false }}>
<div css={styles.header}>
<Typography.Title level={5} style={{ margin: 0 }}>
{locale.title}
</Typography.Title>
<div>
<Button onClick={handleOutput} style={{ marginRight: 8 }}>
{locale.export}
<Modal
open={editModelOpen}
title={locale.editModelTitle}
width={600}
okText={locale.save}
onOk={editSave}
onCancel={editModelClose}
>
<div>
<div style={{ color: 'rgba(0,0,0,0.65)' }}>{locale.editTitle}</div>
<JSONEditor
content={themeConfigContent}
onChange={handleEditConfigChange}
mainMenuBar={false}
/>
</div>
</Modal>
<Button onClick={handleEditConfig} icon={<EditOutlined />} style={{ marginRight: 8 }}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Button onClick={handleEditConfig} icon={<EditOutlined />} style={{ marginRight: 8 }}>
<Button onClick={handleEditConfig} style={{ marginRight: 8 }}>

{locale.edit}
</Button>
<Button onClick={handleReset} style={{ marginRight: 8 }}>
{locale.reset}
Expand Down
1 change: 1 addition & 0 deletions docs/react/recommendation.zh-CN.md
Expand Up @@ -15,6 +15,7 @@ title: 社区精选组件
| 拖拽 | [dnd-kit](https://github.com/clauderic/dnd-kit) [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd/) [react-dnd](https://github.com/gaearon/react-dnd) [react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc) |
| 代码编辑器 | [react-codemirror2](https://github.com/scniro/react-codemirror2) [react-monaco-editor](https://github.com/superRaytin/react-monaco-editor) |
| 富文本编辑器 | [react-quill](https://github.com/zenoamaro/react-quill) [braft-editor](https://github.com/margox/braft-editor) |
| JSON 编辑器 | [vanilla-jsoneditor](https://github.com/josdejong/svelte-jsoneditor) |
| JSON 显示器 | [react-json-view](https://github.com/mac-s-g/react-json-view) |
| 拾色器 | [react-colorful](https://github.com/omgovich/react-colorful) [react-color](http://casesandberg.github.io/react-color/) |
| 响应式 | [react-responsive](https://github.com/contra/react-responsive) [react-media](https://github.com/ReactTraining/react-media) |
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -284,6 +284,7 @@
"ts-node": "^10.8.2",
"typedoc": "^0.23.21",
"typescript": "~4.9.3",
"vanilla-jsoneditor": "^0.11.4",
"webpack-bundle-analyzer": "^4.1.0",
"xhr-mock": "^2.4.1",
"yaml-front-matter": "^4.0.0"
Expand Down