From 59e8b4d56eadba009e456bc3a40c54459e754439 Mon Sep 17 00:00:00 2001 From: zhuzhengjian Date: Sat, 25 Mar 2023 17:49:55 +0800 Subject: [PATCH] feat: add config provider --- components/config-provider/context.ts | 69 +++++++++++++- components/config-provider/css-variables.ts | 94 +++++++++++++++++++ .../config-provider/default-render-empty.tsx | 3 + components/config-provider/index.tsx | 56 +++++++++++ 4 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 components/config-provider/css-variables.ts create mode 100644 components/config-provider/default-render-empty.tsx create mode 100644 components/config-provider/index.tsx diff --git a/components/config-provider/context.ts b/components/config-provider/context.ts index 3f15225..e4319f2 100644 --- a/components/config-provider/context.ts +++ b/components/config-provider/context.ts @@ -1,8 +1,36 @@ -import { createInjectionState } from '@v-c/utils' +import { booleanType, createInjectionState, functionType, objectType, someType, stringType } from '@v-c/utils' +import type { ExtractPropTypes } from 'vue' import { computed } from 'vue' +import type { DerivativeFunc } from '@antd-tiny-vue/cssinjs' +import type { AliasToken, MapToken, OverrideToken, SeedToken } from '../theme/interface' +import type { RenderEmptyHandler } from './default-render-empty' export type SizeType = 'small' | 'middle' | 'large' | undefined +export interface Theme { + primaryColor?: string + infoColor?: string + successColor?: string + processingColor?: string + errorColor?: string + warningColor?: string +} + +export interface CSPConfig { + nonce?: string +} + +export type DirectionType = 'ltr' | 'rtl' | undefined + +export type MappingAlgorithm = DerivativeFunc + +export interface ThemeConfig { + token?: Partial + components?: OverrideToken + algorithm?: MappingAlgorithm | MappingAlgorithm[] + hashed?: boolean + inherit?: boolean +} export const defaultIconPrefixCls = 'anticon' const defaultGetPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => { @@ -10,6 +38,45 @@ const defaultGetPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => return suffixCls ? `ant-${suffixCls}` : 'ant' } + +export const configConsumerProps = { + getTargetContainer: functionType<() => HTMLElement>(), + getPopupContainer: functionType<(triggerNode?: HTMLElement) => HTMLElement>(), + rootPrefixCls: stringType(), + iconPrefixCls: stringType(defaultIconPrefixCls), + getPrefixCls: functionType(defaultGetPrefixCls), + renderEmpty: functionType(), + csp: objectType(), + autoInsertSpaceInButton: booleanType(), + input: objectType<{ + autoComplete?: string + }>(), + pagination: objectType<{ + showSizeChanger?: boolean + }>(), + locale: objectType(), + pageHeader: objectType<{ + ghost: boolean + }>(), + direction: someType([String]), + space: objectType<{ + size?: SizeType | number + }>(), + virtual: booleanType(), + dropdownMatchSelectWidth: booleanType(), + form: objectType<{ + // requiredMark?: RequiredMark + colon?: boolean + // scrollToFirstError?: Options | boolean + }>(), + theme: objectType(), + select: objectType<{ + showSearch?: boolean + }>() +} + +export type ConfigConsumerProps = ExtractPropTypes + const [useProviderConfigProvide, useProviderConfigInject] = createInjectionState(() => { const getPrefixCls = defaultGetPrefixCls const iconPrefixCls = computed(() => defaultIconPrefixCls) diff --git a/components/config-provider/css-variables.ts b/components/config-provider/css-variables.ts new file mode 100644 index 0000000..e32397f --- /dev/null +++ b/components/config-provider/css-variables.ts @@ -0,0 +1,94 @@ +/* eslint-disable import/prefer-default-export, prefer-destructuring */ + +import { generate } from '@ant-design/colors' +import { TinyColor } from '@ctrl/tinycolor' +import { canUseDom, updateCSS, warning } from '@v-c/utils' +import type { Theme } from './context' + +const dynamicStyleMark = `-ant-${Date.now()}-${Math.random()}` + +export function getStyle(globalPrefixCls: string, theme: Theme) { + const variables: Record = {} + + const formatColor = (color: TinyColor, updater?: (cloneColor: TinyColor) => TinyColor) => { + let clone = color.clone() + clone = updater?.(clone) || clone + return clone.toRgbString() + } + + const fillColor = (colorVal: string, type: string) => { + const baseColor = new TinyColor(colorVal) + const colorPalettes = generate(baseColor.toRgbString()) + + variables[`${type}-color`] = formatColor(baseColor) + variables[`${type}-color-disabled`] = colorPalettes[1] + variables[`${type}-color-hover`] = colorPalettes[4] + variables[`${type}-color-active`] = colorPalettes[6] + variables[`${type}-color-outline`] = baseColor.clone().setAlpha(0.2).toRgbString() + variables[`${type}-color-deprecated-bg`] = colorPalettes[0] + variables[`${type}-color-deprecated-border`] = colorPalettes[2] + } + + // ================ Primary Color ================ + if (theme.primaryColor) { + fillColor(theme.primaryColor, 'primary') + + const primaryColor = new TinyColor(theme.primaryColor) + const primaryColors = generate(primaryColor.toRgbString()) + + // Legacy - We should use semantic naming standard + primaryColors.forEach((color, index) => { + variables[`primary-${index + 1}`] = color + }) + // Deprecated + variables['primary-color-deprecated-l-35'] = formatColor(primaryColor, c => c.lighten(35)) + variables['primary-color-deprecated-l-20'] = formatColor(primaryColor, c => c.lighten(20)) + variables['primary-color-deprecated-t-20'] = formatColor(primaryColor, c => c.tint(20)) + variables['primary-color-deprecated-t-50'] = formatColor(primaryColor, c => c.tint(50)) + variables['primary-color-deprecated-f-12'] = formatColor(primaryColor, c => c.setAlpha(c.getAlpha() * 0.12)) + + const primaryActiveColor = new TinyColor(primaryColors[0]) + variables['primary-color-active-deprecated-f-30'] = formatColor(primaryActiveColor, c => c.setAlpha(c.getAlpha() * 0.3)) + variables['primary-color-active-deprecated-d-02'] = formatColor(primaryActiveColor, c => c.darken(2)) + } + + // ================ Success Color ================ + if (theme.successColor) { + fillColor(theme.successColor, 'success') + } + + // ================ Warning Color ================ + if (theme.warningColor) { + fillColor(theme.warningColor, 'warning') + } + + // ================= Error Color ================= + if (theme.errorColor) { + fillColor(theme.errorColor, 'error') + } + + // ================= Info Color ================== + if (theme.infoColor) { + fillColor(theme.infoColor, 'info') + } + + // Convert to css variables + const cssList = Object.keys(variables).map(key => `--${globalPrefixCls}-${key}: ${variables[key]};`) + + return ` + :root { + ${cssList.join('\n')} + } + `.trim() +} + +export function registerTheme(globalPrefixCls: string, theme: Theme) { + const style = getStyle(globalPrefixCls, theme) + + if (canUseDom()) { + updateCSS(style, `${dynamicStyleMark}-dynamic-theme`) + } else { + // @ts-expect-error this is ssr + warning(false, 'ConfigProvider', 'SSR do not support dynamic theme with css variables.') + } +} diff --git a/components/config-provider/default-render-empty.tsx b/components/config-provider/default-render-empty.tsx new file mode 100644 index 0000000..3103cd5 --- /dev/null +++ b/components/config-provider/default-render-empty.tsx @@ -0,0 +1,3 @@ +import type { VNodeChild } from 'vue' + +export type RenderEmptyHandler = (componentName?: string) => VNodeChild diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx new file mode 100644 index 0000000..ef471c2 --- /dev/null +++ b/components/config-provider/index.tsx @@ -0,0 +1,56 @@ +import { booleanType, someType, stringType } from '@v-c/utils' +import type { ExtractPropTypes } from 'vue' +import type { SizeType, Theme } from './context' +import { configConsumerProps, defaultIconPrefixCls } from './context' +import { registerTheme } from './css-variables' + +export const configProviderProps = { + ...configConsumerProps, + prefixCls: stringType(), + componentSize: someType([String]), + componentDisabled: booleanType() +} + +export type ConfigProviderProps = Partial> + +export const defaultPrefixCls = 'ant' +let globalPrefixCls: string +let globalIconPrefixCls: string + +function getGlobalPrefixCls() { + return globalPrefixCls || defaultPrefixCls +} + +function getGlobalIconPrefixCls() { + return globalIconPrefixCls || defaultIconPrefixCls +} + +export const setGlobalConfig = ({ prefixCls, iconPrefixCls, theme }: Pick & { theme?: Theme }) => { + if (prefixCls !== undefined) { + globalPrefixCls = prefixCls + } + if (iconPrefixCls !== undefined) { + globalIconPrefixCls = iconPrefixCls + } + + if (theme) { + registerTheme(getGlobalPrefixCls(), theme) + } +} + +export const globalConfig = () => ({ + getPrefixCls: (suffixCls?: string, customizePrefixCls?: string) => { + if (customizePrefixCls) return customizePrefixCls + return suffixCls ? `${getGlobalPrefixCls()}-${suffixCls}` : getGlobalPrefixCls() + }, + getIconPrefixCls: getGlobalIconPrefixCls, + getRootPrefixCls: () => { + // If Global prefixCls provided, use this + if (globalPrefixCls) { + return globalPrefixCls + } + + // Fallback to default prefixCls + return getGlobalPrefixCls() + } +})