Skip to content

Feat v4 theme editor #6348

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

Merged
merged 9 commits into from
Mar 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add theme editor layout
  • Loading branch information
shifeng1993 committed Mar 6, 2023
commit 93c9c2b372ea033284bb90a5bf90db39eeac3261
166 changes: 166 additions & 0 deletions site/src/components/antdv-token-previewer/ThemeEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import type { PropType } from 'vue';
import { defineComponent, toRefs, ref, computed } from 'vue';
import type { DerivativeFunc } from 'ant-design-vue/es/_util/cssinjs';
import classNames from 'ant-design-vue/es/_util/classNames';
// import { antdComponents } from './component-panel';
import useControlledTheme from './hooks/useControlledTheme';
import type { SelectedToken, Theme } from './interface';
import type { Locale } from './locale';
import { useProvideLocaleContext, zhCN } from './locale';
import { mapRelatedAlias, seedRelatedAlias, seedRelatedMap } from './meta/TokenRelation';
// import type { TokenPanelProProps } from './token-panel-pro';
// import TokenPanelPro from './token-panel-pro';
// import ComponentDemoPro from './token-panel-pro/ComponentDemoPro';
import makeStyle from './utils/makeStyle';
import { getRelatedComponents } from './utils/statistic';

const useStyle = makeStyle('ThemeEditor', token => ({
'.antd-theme-editor': {
backgroundColor: token.colorBgLayout,
display: 'flex',
},
}));

const defaultTheme: Theme = {
name: '默认主题',
key: 'default',
config: {},
};

export type ThemeEditorProps = {
/**
* @deprecated
* @default true
*/
simple?: boolean;
theme?: Theme;
onThemeChange?: (theme: Theme) => void;
darkAlgorithm?: DerivativeFunc<any, any>;
locale?: Locale;
};

const ThemeEditor = defineComponent({
name: 'ThemeEditor',
inheritAttrs: false,
props: {
simple: { type: Boolean },
theme: { type: Object as PropType<Theme> },
darkAlgorithm: { type: Function as PropType<DerivativeFunc<any, any>> },
locale: { type: Object as PropType<Locale>, default: zhCN },
},
emits: ['themeChange'],
setup(props, { attrs, emit, expose }) {
const { theme: customTheme, darkAlgorithm, locale } = toRefs(props);

const [wrapSSR, hashId] = useStyle();

const selectedTokens = ref<SelectedToken>({
seed: ['colorPrimary'],
});

const aliasOpen = ref<boolean>(false);

const { theme, infoFollowPrimary, onInfoFollowPrimaryChange } = useControlledTheme({
theme: customTheme,
defaultTheme,
onChange: (theme: Theme) => emit('themeChange', theme),
darkAlgorithm,
});

const handleTokenSelect: (token: string | string[], type: keyof SelectedToken) => void = (
token,
type,
) => {
const tokens = typeof token === 'string' ? (token ? [token] : []) : token;
if (type === 'seed') {
return {
seed: tokens,
};
}

let newSelectedTokens = { ...selectedTokens.value };
tokens.forEach(newToken => {
newSelectedTokens = {
...selectedTokens.value,
[type]: selectedTokens.value[type]?.includes(newToken)
? selectedTokens.value[type]?.filter(t => t !== newToken)
: [...(selectedTokens.value[type] ?? []), newToken],
};
});
if (type === 'map') {
delete newSelectedTokens.alias;
}
selectedTokens.value = newSelectedTokens;
};

const computedSelectedTokens = computed(() => {
if (
selectedTokens.value.seed?.length &&
!selectedTokens.value.map?.length &&
!selectedTokens.value.alias?.length
) {
return [
...selectedTokens.value.seed,
...((seedRelatedMap as any)[selectedTokens.value.seed[0]] ?? []),
...((seedRelatedAlias as any)[selectedTokens.value.seed[0]] ?? []),
];
}
if (selectedTokens.value.map?.length && !selectedTokens.value.alias?.length) {
return [
...selectedTokens.value.map,
...selectedTokens.value.map.reduce((result, item) => {
return result.concat((mapRelatedAlias as any)[item]);
}, []),
];
}
if (selectedTokens.value.alias?.length) {
return [...selectedTokens.value.alias];
}
return [];
});

const relatedComponents = computed(() => {
return computedSelectedTokens.value ? getRelatedComponents(computedSelectedTokens.value) : [];
});

useProvideLocaleContext(locale);

return () => {
return wrapSSR(
<div {...attrs} class={classNames(hashId.value, 'antd-theme-editor', attrs.class)}>
<div
style={{
flex: aliasOpen.value ? '0 0 860px' : `0 0 ${860 - 320}px`,
height: '100%',
backgroundColor: '#F7F8FA',
backgroundImage: 'linear-gradient(180deg, #FFFFFF 0%, rgba(246,247,249,0.00) 100%)',
display: 'flex',
transition: 'all 0.3s',
}}
>
{/* <TokenPanelPro
aliasOpen={aliasOpen.value}
onAliasOpenChange={open => aliasOpen.value = open}
theme={theme}
style={{ flex: 1 }}
selectedTokens={selectedTokens.value}
onTokenSelect={handleTokenSelect}
infoFollowPrimary={infoFollowPrimary.value}
onInfoFollowPrimaryChange={onInfoFollowPrimaryChange}
/> */}
</div>
{/* <ComponentDemoPro
theme={theme}
components={antdComponents}
activeComponents={relatedComponents.value}
selectedTokens={computedSelectedTokens.value}
style={{ flex: 1, overflow: 'auto', height: '100%' }}
componentDrawer
/> */}
</div>,
);
};
},
});

export default ThemeEditor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { DerivativeFunc } from 'ant-design-vue/es/_util/cssinjs';
import { theme as antTheme } from 'ant-design-vue';
import type { ThemeConfig } from 'ant-design-vue/es/config-provider/context';
import type { Ref } from 'vue';
import { watchEffect, ref, computed } from 'vue';
import type { MutableTheme, Theme } from '../interface';
import deepUpdateObj from '../utils/deepUpdateObj';
import getDesignToken from '../utils/getDesignToken';
import getValueByPath from '../utils/getValueByPath';

const { darkAlgorithm: defaultDark, compactAlgorithm, defaultAlgorithm } = antTheme;

export type ThemeCode = 'default' | 'dark' | 'compact';
export const themeMap: Record<ThemeCode, DerivativeFunc<any, any>> = {
dark: defaultDark,
compact: compactAlgorithm,
default: defaultAlgorithm,
};

export type SetThemeState = (theme: Theme, modifiedPath: string[], updated?: boolean) => void;

export type UseControlledTheme = (options: {
theme?: Ref<Theme>;
defaultTheme: Theme;
onChange?: (theme: Theme) => void;
darkAlgorithm?: Ref<DerivativeFunc<any, any>>;
}) => {
theme: Ref<MutableTheme>;
infoFollowPrimary: Ref<boolean>;
onInfoFollowPrimaryChange: (value: boolean) => void;
updateRef: () => void;
};

const useControlledTheme: UseControlledTheme = ({ theme: customTheme, defaultTheme, onChange }) => {
const theme = ref<Theme>(customTheme.value ?? defaultTheme);
const infoFollowPrimary = ref<boolean>(false);
const themeRef = ref<Theme>(theme.value);
const renderHolder = ref(0);

const forceUpdate = () => (renderHolder.value = renderHolder.value + 1);

const getNewTheme = (newTheme: Theme, force?: boolean): Theme => {
const newToken = { ...newTheme.config.token };
if (infoFollowPrimary || force) {
newToken.colorInfo = getDesignToken(newTheme.config).colorPrimary;
}
return { ...newTheme, config: { ...newTheme.config, token: newToken } };
};

const handleSetTheme: SetThemeState = newTheme => {
if (customTheme.value) {
onChange?.(getNewTheme(newTheme));
} else {
theme.value = getNewTheme(newTheme);
}
};

const handleResetTheme = (path: string[]) => {
let newConfig = { ...theme.value.config };
newConfig = deepUpdateObj(newConfig, path, getValueByPath(themeRef.value?.config, path));
handleSetTheme({ ...theme.value, config: newConfig }, path);
};

const getCanReset = (origin: ThemeConfig, current: ThemeConfig) => (path: string[]) => {
return getValueByPath(origin, path) !== getValueByPath(current, path);
};

// Controlled theme change
watchEffect(() => {
if (customTheme.value) {
theme.value = customTheme.value;
}
});

const handleInfoFollowPrimaryChange = (value: boolean) => {
infoFollowPrimary.value = value;
if (value) {
theme.value = getNewTheme(theme.value, true);
}
};

return {
theme: computed(() => ({
...theme.value,
onThemeChange: (config, path) => handleSetTheme({ ...theme.value, config }, path),
onReset: handleResetTheme,
getCanReset: getCanReset(themeRef.value?.config, theme.value.config),
})),
infoFollowPrimary,
onInfoFollowPrimaryChange: handleInfoFollowPrimaryChange,
updateRef: () => {
themeRef.value = theme.value;
forceUpdate();
},
};
};

export default useControlledTheme;
2 changes: 2 additions & 0 deletions site/src/components/antdv-token-previewer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './locale';
export { default as ThemeEditor } from './ThemeEditor';
38 changes: 38 additions & 0 deletions site/src/components/antdv-token-previewer/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ThemeConfig } from 'ant-design-vue/es/config-provider/context';
import { VueNode } from 'ant-design-vue/es/_util/type';

export type Theme = {
name: string;
key: string;
config: ThemeConfig;
};

export type AliasToken = Exclude<ThemeConfig['token'], undefined>;
export type TokenValue = string | number | string[] | number[] | boolean;
export type TokenName = keyof AliasToken;

// 修改线 以上都是改过的
export interface ComponentDemo {
tokens?: TokenName[];
demo: VueNode;
key: string;
}

export interface MutableTheme extends Theme {
onThemeChange?: (newTheme: ThemeConfig, path: string[]) => void;
onReset?: (path: string[]) => void;
getCanReset?: (path: string[]) => boolean;
}

export type PreviewerProps = {
onSave?: (themeConfig: ThemeConfig) => void;
showTheme?: boolean;
theme?: Theme;
onThemeChange?: (config: ThemeConfig) => void;
};

export type SelectedToken = {
seed?: string[];
map?: string[];
alias?: string[];
};
19 changes: 19 additions & 0 deletions site/src/components/antdv-token-previewer/locale/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Ref, InjectionKey } from 'vue';
import { inject, provide, computed } from 'vue';

import type { Locale } from './interface';
import zhCN from './zh-CN';

const contextKey: InjectionKey<Ref<Locale>> = Symbol('localeContext');

export const useProvideLocaleContext = (props: Ref<Locale>) => {
provide(contextKey, props);
return props;
};

export const useInjectLocaleContext = () => {
return inject(
contextKey,
computed(() => zhCN),
);
};
20 changes: 20 additions & 0 deletions site/src/components/antdv-token-previewer/locale/en-US.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Locale } from './interface';

const locale: Locale = {
_lang: 'en-US',
followPrimary: 'Follow Brand Color',
reset: 'Reset',
next: 'Next',
groupView: 'Group View',
fill: 'Fill',
border: 'Border',
background: 'Background',
text: 'Text',
demo: {
overview: 'Overview',
components: 'Components',
relatedTokens: 'Related Tokens',
},
};

export default locale;
4 changes: 4 additions & 0 deletions site/src/components/antdv-token-previewer/locale/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { useProvideLocaleContext, useInjectLocaleContext } from './context';
export { default as enUS } from './en-US';
export type { Locale } from './interface';
export { default as zhCN } from './zh-CN';
16 changes: 16 additions & 0 deletions site/src/components/antdv-token-previewer/locale/interface.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type Locale = {
_lang: string;
followPrimary: string;
reset: string;
next: string;
groupView: string;
fill: string;
background: string;
border: string;
text: string;
demo: {
overview: string;
components: string;
relatedTokens: string;
};
};
20 changes: 20 additions & 0 deletions site/src/components/antdv-token-previewer/locale/zh-CN.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Locale } from './interface';

const locale: Locale = {
_lang: 'zh-CN',
followPrimary: '跟随主色',
reset: '重置',
next: '下一步',
groupView: '分组显示',
fill: '填充',
border: '描边',
background: '背景',
text: '文本',
demo: {
overview: '概览',
components: '组件',
relatedTokens: '关联 Token',
},
};

export default locale;
Loading