Compare commits

...

18 Commits

Author SHA1 Message Date
selicens a15bb9cfb8
refactor(flex): use SFC (#8308)
* refactor(flex): use SFC

* test: add unit tests
2025-08-23 09:21:35 +08:00
tangjinzhou 5cc4a63480 feat: add affix 2025-08-10 13:52:32 +08:00
tangjinzhou bacb790bfb refactor: update Button component props to use TypeScript types and default values 2025-08-10 13:52:14 +08:00
tangjinzhou 5a768e2ae4 test: add comprehensive tests for variants, sizes, and states 2025-08-04 21:50:23 +08:00
tangjinzhou 30afa132a2 chore: add testing and coverage configuration with Vitest integration 2025-08-04 15:04:06 +08:00
tangjinzhou 23ebeea4b7 feat: add wave animation 2025-07-31 15:48:39 +08:00
tangjinzhou d7ca354b87 feat: implement theme toggling and enhance Button styles with new color variables 2025-07-30 23:02:30 +08:00
tangjinzhou 428fdfc182 feat: add Theme component and enhance Button with href and target props 2025-07-30 19:31:43 +08:00
tangjinzhou 73dd6d7625 fix: update type definitions path in package.json 2025-07-29 22:11:33 +08:00
tangjinzhou 2cf758d770 feat: enhance Button component with new variants, sizes, and styles 2025-07-29 19:53:16 +08:00
tangjinzhou 136543398d feat: add Input component and update Button styles 2025-07-29 11:23:54 +08:00
tangjinzhou 8f73247bed feat: integrate @ant-design-vue/tailwind-config and update styles for UI components 2025-07-29 11:22:59 +08:00
tangjinzhou 36a6ed5e34 feat: add Tailwind CSS integration and define base styles 2025-07-28 21:27:23 +08:00
tangjinzhou ec4017d86f refactor: replace ant-design-vue with @ant-design-vue/ui 2025-07-28 21:27:09 +08:00
tangjinzhou 8d33f2f843 feat: update configuration for aliasing 2025-07-27 16:40:12 +08:00
tangjinzhou 312f82a5b6 refactor: remove React dependencies and streamline Vite configuration for Vue application 2025-07-27 15:30:36 +08:00
tangjinzhou c49a9aa6c3 chore: use monorepo 2025-07-24 22:06:11 +08:00
tangjinzhou f5560db05d
Feat 4.3 (#8286)
* feat(cssvar): Migrate cssvar features of antd v5.13.2 version (#7220)

* feat(cssinjs): update cssinjs to v1.18.1

* feat(theme): update theme dir to V5.12.5

* feat(theme&configprovider): update cssvar to v5.12.5

* feat(site): add site cssvar example

* feat(cssinjs): update cssinjs to v1.81.2

* feat(theme): update theme dir to V5.13.2

* fix: component ComponentToken export

* fix: cssvar in the component being loaded multiple times.

* feat(button): add cssvar to the Button component.

* fix: css var

* refactor: rename cssinjs folder

* feat: alert support cssvar

* feat: affix support cssvar

* feat: anchor support cssvar

* feat: switch support cssvar (#7940)

---------

Co-authored-by: 果冻橙 <shifeng199307@gmail.com>
2025-07-24 10:56:14 +08:00
687 changed files with 7503 additions and 30884 deletions

View File

@ -1,36 +0,0 @@
const fs = require('fs');
const path = require('path');
const restCssPath = path.join(process.cwd(), 'components', 'style', 'reset.css');
const tokenStatisticPath = path.join(process.cwd(), 'components', 'version', 'token.json');
const tokenMetaPath = path.join(process.cwd(), 'components', 'version', 'token-meta.json');
function finalizeCompile() {
if (fs.existsSync(path.join(__dirname, './es'))) {
fs.copyFileSync(restCssPath, path.join(process.cwd(), 'es', 'style', 'reset.css'));
fs.copyFileSync(tokenStatisticPath, path.join(process.cwd(), 'es', 'version', 'token.json'));
fs.copyFileSync(tokenMetaPath, path.join(process.cwd(), 'es', 'version', 'token-meta.json'));
}
if (fs.existsSync(path.join(__dirname, './lib'))) {
fs.copyFileSync(restCssPath, path.join(process.cwd(), 'lib', 'style', 'reset.css'));
fs.copyFileSync(tokenStatisticPath, path.join(process.cwd(), 'lib', 'version', 'token.json'));
fs.copyFileSync(tokenMetaPath, path.join(process.cwd(), 'lib', 'version', 'token-meta.json'));
}
}
function finalizeDist() {
if (fs.existsSync(path.join(__dirname, './dist'))) {
fs.copyFileSync(restCssPath, path.join(process.cwd(), 'dist', 'reset.css'));
}
}
module.exports = {
compile: {
finalize: finalizeCompile,
},
dist: {
finalize: finalizeDist,
},
bail: true,
};

View File

@ -1,2 +0,0 @@
codecov:
branch: master

View File

@ -1,11 +0,0 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -1,13 +0,0 @@
node_modules/
**/*.spec.*
**/style/
*.html
/components/test/*
es/
lib/
_site/
dist/
site/dist/
components/version/version.ts
site/src/router/demoRoutes.js
locale/

View File

@ -1,112 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
jasmine: true,
jest: true,
es6: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
parser: 'babel-eslint',
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'@vue/typescript/recommended',
'@vue/prettier',
// 'prettier',
],
// extends: [
// 'eslint:recommended',
// 'plugin:vue/vue3-recommended',
// '@vue/typescript/recommended',
// '@vue/prettier',
// ],
plugins: ['markdown', 'jest', '@typescript-eslint', 'import'],
globals: {
h: true,
defineProps: 'readonly',
},
overrides: [
{
files: ['*.md'],
processor: 'markdown/markdown',
rules: {
'no-console': 'off',
},
},
{
files: ['*.ts', '*.tsx'],
// extends: ['@vue/typescript/recommended', '@vue/prettier'],
parserOptions: {
project: './tsconfig.json',
},
rules: {
'@typescript-eslint/ban-types': 0,
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-empty-function': 0,
'@typescript-eslint/no-non-null-assertion': 0,
'@typescript-eslint/no-unused-vars': [
'error',
{ vars: 'all', args: 'after-used', ignoreRestSiblings: true },
],
'@typescript-eslint/ban-ts-comment': 0,
},
},
{
files: ['*.vue'],
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2021,
},
rules: {
'no-console': 'off',
'vue/no-reserved-component-names': 'off',
},
},
],
rules: {
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-empty-function': 0,
'@typescript-eslint/no-unused-vars': [
'error',
{ vars: 'all', args: 'after-used', ignoreRestSiblings: true, argsIgnorePattern: '^_' },
],
'import/no-named-as-default': 'off',
'import/namespace': [2, { allowComputed: true }],
'import/no-named-as-default-member': 'off',
'import/no-unresolved': [2, { ignore: ['ant-design-vue'] }],
'comma-dangle': [2, 'always-multiline'],
'no-var': 'error',
'no-console': [2, { allow: ['warn', 'error'] }],
'object-shorthand': 2,
'no-unused-vars': [2, { ignoreRestSiblings: true, argsIgnorePattern: '^_' }],
'no-undef': 2,
camelcase: 'off',
'no-extra-boolean-cast': 'off',
semi: ['error', 'always'],
'vue/no-v-html': 'off',
'vue/require-explicit-emits': 'off',
'vue/require-prop-types': 'off',
'vue/require-default-prop': 'off',
'vue/no-reserved-keys': 'off',
'vue/comment-directive': 'off',
'vue/prop-name-casing': 'off',
'vue/one-component-per-file': 'off',
'vue/custom-event-name-casing': 'off',
'vue/v-on-event-hyphenation': 'off',
'vue/max-attributes-per-line': [
2,
{
singleline: 20,
multiline: 1,
},
],
'vue/multi-word-component-names': 'off',
},
};

44
.gitignore vendored
View File

@ -66,9 +66,6 @@ package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
/coverage /coverage
# 备份文件
/components/test/*
list.txt
site/dev.js site/dev.js
@ -77,10 +74,39 @@ vetur/
report.html report.html
site/src/router/demoRoutes.js
components/version/version.ts
components/version/version.tsx # Local env files
components/version/token.json .env
components/version/token-meta.json .env.*
~component-api.json !.env.template
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
.next/
out/
build/
dist/
storybook-static/
# Debug
npm-debug.log*
# Misc
.DS_Store
*.pem
vite.config.*.timestamp*
*.tsbuildinfo
*.log
.npmrc
.tsup/

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install pretty-quick --staged

View File

@ -1,7 +0,0 @@
{
"hooks": {
"pre-commit": "pretty-quick --staged",
"pre-publish": "npm run lint",
"commit-msg": "commitlint -x @commitlint/config-conventional -e $GIT_PARAMS"
}
}

View File

@ -1,60 +0,0 @@
const libDir = process.env.LIB_DIR;
const transformIgnorePatterns = [
'/dist/',
// Ignore modules without es dir.
// Update: @babel/runtime should also be transformed
// 'node_modules/(?!.*(@babel|lodash-es))',
'node_modules/(?!@ant-design/icons-vue|@ant-design/icons-svg|lodash-es)/',
];
const testPathIgnorePatterns = ['/node_modules/', 'node'];
function getTestRegex(libDir) {
if (libDir === 'dist') {
return 'demo\\.test\\.js$';
}
return '.*\\.test\\.(j|t)sx?$';
}
module.exports = {
verbose: true,
setupFiles: ['./tests/setup.js'],
setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'],
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'vue', 'md', 'jpg'],
modulePathIgnorePatterns: ['/_site/'],
testPathIgnorePatterns: testPathIgnorePatterns,
transform: {
'\\.(vue|md)$': '<rootDir>/node_modules/@vue/vue3-jest',
'\\.(js|jsx)$': '<rootDir>/node_modules/babel-jest',
'\\.(ts|tsx)$': '<rootDir>/node_modules/ts-jest',
'\\.svg$': '<rootDir>/node_modules/jest-transform-stub',
},
testRegex: getTestRegex(libDir),
moduleNameMapper: {
'^@/(.*)$/': '<rootDir>/$1',
'^ant-design-vue$': '<rootDir>/components/index',
'^ant-design-vue/es/(.*)$': '<rootDir>/components/$1',
},
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
collectCoverage: process.env.COVERAGE === 'true',
collectCoverageFrom: [
'components/**/*.{js,jsx,vue}',
'!components/*/__tests__/**/type.{js,jsx}',
'!components/vc-*/**/*',
'!components/*/demo/**/*',
'!components/_util/**/*',
'!components/align/**/*',
'!components/trigger/**/*',
'!**/node_modules/**',
],
testEnvironment: 'jsdom',
testEnvironmentOptions: {
url: 'http://localhost',
customExportConditions: ['node', 'node-addons'],
},
transformIgnorePatterns,
globals: {
'ts-jest': {
babelConfig: true,
},
},
};

View File

@ -1,31 +0,0 @@
**/*.svg
lib/
es/
dist/
_site/
coverage/
CNAME
LICENSE
yarn.lock
netlify.toml
yarn-error.log
*.sh
*.snap
.gitignore
.npmignore
.prettierignore
.DS_Store
.editorconfig
.eslintignore
**/*.yml
**/assets
.gitattributes
.stylelintrc
.vcmrc
.png
.npmrc.template
.huskyrc
.gitmodules
*.png
v2-doc/

View File

@ -1,17 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf",
"printWidth": 100,
"proseWrap": "never",
"arrowParens": "avoid",
"htmlWhitespaceSensitivity": "ignore",
"overrides": [
{
"files": ".prettierrc",
"options": {
"parser": "json"
}
}
]
}

View File

@ -1,23 +0,0 @@
{
"extends": ["stylelint-config-standard", "stylelint-config-prettier"],
"rules": {
"comment-empty-line-before": null,
"declaration-empty-line-before": null,
"function-comma-newline-after": null,
"function-name-case": null,
"function-parentheses-newline-inside": null,
"function-max-empty-lines": null,
"function-whitespace-after": null,
"indentation": null,
"number-leading-zero": null,
"number-no-trailing-zeros": null,
"rule-empty-line-before": null,
"selector-combinator-space-after": null,
"selector-list-comma-newline-after": null,
"selector-pseudo-element-colon-notation": null,
"unit-no-unknown": null,
"value-list-max-empty-lines": null,
"font-family-no-missing-generic-family-keyword": null,
"no-descending-specificity": null
}
}

View File

@ -1,43 +0,0 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-rational-order",
"stylelint-config-prettier"
],
"customSyntax": "postcss-less",
"plugins": ["stylelint-declaration-block-no-ignored-properties"],
"rules": {
"function-name-case": ["lower"],
"function-no-unknown": [
true,
{
"ignoreFunctions": [
"fade",
"fadeout",
"tint",
"darken",
"ceil",
"fadein",
"floor",
"unit",
"shade",
"lighten",
"percentage",
"-"
]
}
],
"import-notation": null,
"no-descending-specificity": null,
"no-invalid-position-at-import-rule": null,
"declaration-empty-line-before": null,
"keyframes-name-pattern": null,
"custom-property-pattern": null,
"number-max-precision": 8,
"alpha-value-notation": "number",
"color-function-notation": "legacy",
"selector-class-pattern": null,
"selector-id-pattern": null,
"selector-not-notation": null
}
}

17
.vcmrc
View File

@ -1,17 +0,0 @@
{
"helpMessage": "\nPlease fix your commit message (and consider using https://www.npmjs.com/package/commitizen)\n",
"types": [
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"chore",
"revert",
"ci"
],
"warnOnFail": false,
"autoFix": false
}

View File

@ -0,0 +1,15 @@
@import '@ant-design-vue/tailwind-config';
@source '../index.html';
@source '../src/**/*.{vue,ts}';
* {
scrollbar-width: thin;
scrollbar-color: var(--color-base-300) transparent;
}
*:focus-visible {
outline: none;
}
.shiki.github-dark,
.dark-scrollbar {
scrollbar-color: rgba(121, 121, 121, 0.4) transparent;
}

View File

@ -0,0 +1,3 @@
// @ts-check
export { default } from '@ant-design-vue/eslint-config/vue'

View File

View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Playground</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,45 @@
{
"name": "playground",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite",
"lint": "eslint . --fix",
"preview": "vite preview",
"tsc": "vue-tsc --noEmit"
},
"dependencies": {
"@floating-ui/vue": "^1.1.5",
"@heroicons/vue": "^2.1.5",
"@ant-design-vue/ui": "*",
"@simonwep/pickr": "^1.9.1",
"@trpc/client": "^11.0.0",
"@trpc/server": "^11.0.0",
"@wdns/vue-code-block": "^2.3.3",
"clsx": "^2.1.1",
"cookies": "^0.9.1",
"uuid": "^10.0.0",
"vue": "^3.4.34",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@ant-design-vue/eslint-config": "*",
"@ant-design-vue/prettier-config": "*",
"@ant-design-vue/typescript-config": "*",
"@ant-design-vue/vite-config": "*",
"@ant-design-vue/tailwind-config": "*",
"@tailwindcss/vite": "^4.1.3",
"@types/cookies": "^0.9.0",
"@types/node": "^20.0.0",
"@vitejs/plugin-vue": "^5.1.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.1.3",
"typescript": "^5.8.2",
"vite": "^5.3.5",
"vite-plugin-dts": "^3.9.1",
"vite-svg-loader": "^5.1.0",
"vue-tsc": "^3.0.3"
}
}

View File

@ -0,0 +1,3 @@
// @ts-check
export { default } from "@ant-design-vue/prettier-config/tailwind";

View File

@ -0,0 +1,15 @@
<template>
<button @click="toggleTheme" class="fixed top-2 right-2">toggle {{ appearance }}</button>
<a-theme :appearance="appearance">
<RouterView />
</a-theme>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const appearance = ref('light')
const toggleTheme = () => {
appearance.value = appearance.value === 'light' ? 'dark' : 'light'
}
</script>

View File

@ -0,0 +1,29 @@
<template>
<div class="flex h-screen" :class="pageClass">
<TheNavbar v-if="!hideNavbar" :items="navs"></TheNavbar>
<div class="flex-1 justify-center px-4 py-16" :class="contentClass">
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup>
import { provideLayoutOptions } from '@/composables/layout'
import { computed, ref } from 'vue'
import TheNavbar from './TheNavbar.vue'
const props = defineProps<{
navs: { name: string; path: string }[]
hideNavbar?: boolean
hideBreadcrumbs?: boolean
}>()
const pageClass = ref<string>()
const contentClass = ref<string>()
provideLayoutOptions({
pageClass,
contentClass,
hideNavbar: computed(() => props.hideNavbar),
hideBreadcrumbs: computed(() => props.hideBreadcrumbs),
})
</script>

View File

@ -0,0 +1,25 @@
<template>
<div class="flex flex-wrap gap-8 px-8">
<RouterLink v-for="item in items" :key="item.path" :to="item.path">
<div
class="shadow-xs relative flex w-72 flex-col rounded-lg bg-primary capitalize text-primary-content transition-all hover:scale-105 hover:shadow-xl"
>
<div className="flex-col gap-2 flex flex-auto p-4 text-sm items-center text-center">
<h2 className="font-semibold flex items-center gap-2 text-xl mb-1">
{{ item.name }}
</h2>
<div className="flex flex-wrap items-start gap-2 justify-end">
<ArrowRightIcon class="size-5" />
</div>
</div>
</div>
</RouterLink>
</div>
</template>
<script lang="ts" setup>
import { ArrowRightIcon } from '@heroicons/vue/20/solid'
defineProps<{
items: { name: string; path: string }[]
}>()
</script>

View File

@ -0,0 +1,29 @@
<template>
<div class="max-w-full select-none overflow-x-auto px-4 py-2 text-sm">
<ul class="flex min-h-min items-center whitespace-nowrap capitalize">
<template v-for="(item, i) in items" :key="item.path">
<li class="flex items-center">
<template v-if="i > 0">
<span
class="ml-2 mr-3 block size-1.5 rotate-45 transform border-r-[1px] border-t-[1px] border-base-content/70 bg-transparent"
></span>
</template>
<template v-if="item.path !== route.path">
<RouterLink class="flex items-center hover:underline" :to="item.path">
{{ item.name }}
</RouterLink>
</template>
<template v-else>
<span class="text-base-content/70">{{ item.name }}</span>
</template>
</li>
</template>
</ul>
</div>
</template>
<script lang="ts" setup>
import { useRoute } from 'vue-router'
defineProps<{ items: { name: string; path: string }[] }>()
const route = useRoute()
</script>

View File

@ -0,0 +1,44 @@
<template>
<div class="bg-base-100/90 text-base-content border-base-content/10 border-r">
<div class="flex min-h-16 w-full items-center p-2">
<div class="justify-start">
<div class="group relative inline-block">
<ul
tabindex="0"
class="bg-base-100 z-[1] mt-3 flex w-52 origin-top scale-95 flex-col flex-wrap rounded-lg p-2 text-sm capitalize"
>
<li v-for="item in items" :key="item.name">
<RouterLink
:aria-disabled="item.path === route.path"
:to="item.path"
@click.stop="$event.currentTarget.blur()"
class="hover:bg-base-content/10 flex cursor-pointer flex-col rounded-lg px-3 py-2 transition duration-200"
>
{{ item.name }}
</RouterLink>
<ul v-if="item.children">
<li v-for="child in item.children" :key="child.name">
<RouterLink
:to="child.path"
@click.stop="$event.currentTarget.blur()"
class="hover:bg-base-content/10 flex cursor-pointer flex-col rounded-lg px-3 py-2 text-xs opacity-80 transition duration-200"
>
{{ child.name }}
</RouterLink>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRoute } from 'vue-router'
defineProps<{
items: { name: string; path: string; children?: { name: string; path: string }[] }[]
}>()
const route = useRoute()
</script>

View File

@ -0,0 +1,22 @@
import { inject, InjectionKey, provide, Ref } from 'vue'
export interface LayoutOptions {
pageClass: Ref<string | undefined>
contentClass: Ref<string | undefined>
hideNavbar: Ref<boolean>
hideBreadcrumbs: Ref<boolean>
}
const LayoutOptionsToken: InjectionKey<LayoutOptions> = Symbol()
export function provideLayoutOptions(options: LayoutOptions) {
provide(LayoutOptionsToken, options)
}
export function injectLayoutOptions() {
const options = inject(LayoutOptionsToken)
if (!options) {
throw new Error('"injectLayoutOptions" must be called inside pages')
}
return options
}

View File

@ -0,0 +1,15 @@
import '~/tailwind.css'
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import routes from './routes'
import antd from '@ant-design-vue/ui'
import '@ant-design-vue/ui/tailwind.css'
import '@ant-design-vue/ui/style.css'
const router = createRouter({
history: createWebHistory(),
routes,
})
createApp(App).use(router).use(antd).mount('#app')

View File

@ -0,0 +1,20 @@
<template>
<div class="flex h-[200vh] flex-col gap-2">
<a-affix :offset-top="top" @change="onChange">
<a-button type="primary" @click="top += 10">Affix top</a-button>
</a-affix>
<br />
<a-affix :offset-bottom="bottom">
<a-button type="primary" @click="bottom += 10">Affix bottom</a-button>
</a-affix>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const top = ref<number>(10)
const bottom = ref<number>(10)
const onChange = (lastAffix: boolean) => {
console.log('onChange', lastAffix)
}
</script>

View File

@ -0,0 +1,56 @@
<template>
<div class="flex flex-wrap gap-2">
<a-button variant="solid" size="lg">Solid Button</a-button>
<a-button variant="solid" size="lg" disabled>Solid Button</a-button>
<a-button variant="solid" size="lg" loading>Solid Button</a-button>
<a-button variant="solid" size="lg" danger>Danger Solid Button</a-button>
<a-button variant="solid" size="lg" danger disabled>Disabled Danger Solid Button</a-button>
<br />
<a-button variant="outlined" size="sm">Outlined Button</a-button>
<a-button variant="outlined" size="md">Outlined Button</a-button>
<a-button variant="outlined" size="lg">Outlined Button</a-button>
<a-button variant="outlined" size="lg" disabled>Disabled Outlined Button</a-button>
<a-button variant="outlined" size="lg" loading>Loading Outlined Button</a-button>
<a-button variant="outlined" size="lg" danger>Danger Outlined Button</a-button>
<a-button variant="outlined" size="lg" danger disabled>
Disabled Danger Outlined Button
</a-button>
<br />
<a-button variant="text" size="sm">Text Button</a-button>
<a-button variant="text" size="md">Text Button</a-button>
<a-button variant="text" size="lg">Text Button</a-button>
<a-button variant="text" size="lg" disabled>Disabled Text Button</a-button>
<a-button variant="text" size="lg" loading>Loading Text Button</a-button>
<a-button variant="text" size="lg" danger>Danger Text Button</a-button>
<a-button variant="text" size="lg" danger disabled>Disabled Danger Text Button</a-button>
<br />
<a-button variant="link" size="sm">Link Button</a-button>
<a-button variant="link" size="md">Link Button</a-button>
<a-button variant="link" size="lg">Link Button</a-button>
<a-button variant="link" size="lg" disabled>Disabled Link Button</a-button>
<a-button variant="link" size="lg" loading>Loading Link Button</a-button>
<a-button variant="link" size="lg" danger>Danger Link Button</a-button>
<a-button variant="link" size="lg" danger disabled>Disabled Danger Link Button</a-button>
<br />
<a-button variant="dashed" size="sm">Dashed Button</a-button>
<a-button variant="dashed" size="md">Dashed Button</a-button>
<a-button variant="dashed" size="lg">Dashed Button</a-button>
<a-button variant="dashed" size="lg" disabled>Disabled Dashed Button</a-button>
<a-button variant="dashed" size="lg" loading>Loading Dashed Button</a-button>
<a-button variant="dashed" size="lg" danger>Danger Dashed Button</a-button>
<a-button variant="dashed" size="lg" danger disabled>Disabled Danger Dashed Button</a-button>
<br />
<a-button variant="filled" size="sm">Filled Button</a-button>
<a-button variant="filled" size="md">Filled Button</a-button>
<a-button variant="filled" size="lg">Filled Button</a-button>
<a-button variant="filled" size="lg" disabled>Disabled Filled Button</a-button>
<a-button variant="filled" size="lg" loading>Loading Filled Button</a-button>
<a-button variant="filled" size="lg" danger>Danger Filled Button</a-button>
<a-button variant="filled" size="lg" danger disabled>Disabled Danger Filled Button</a-button>
<a-button color="purple">Purple Button</a-button>
<a-button color="blue">Blue Button</a-button>
<a-button color="green">Green Button</a-button>
<a-button color="red">Red Button</a-button>
</div>
</template>

View File

@ -0,0 +1,12 @@
<template>
<div class="flex flex-col gap-2">
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Default</button>
<button class="btn btn-error">Danger</button>
<button class="btn btn-link">Link</button>
<button class="btn btn-ghost">Ghost</button>
<button class="btn btn-link">Link</button>
<button class="btn btn-link">Link</button>
<button class="btn btn-link">Link</button>
</div>
</template>

View File

@ -0,0 +1,93 @@
<template>
<a-flex gap="middle" vertical>
<label>
Select axis:
<select v-model="axis">
<option v-for="item in axisOptions" :key="item">{{ item }}</option>
</select>
</label>
<a-flex :vertical="axis === 'vertical'">
<div
v-for="(item, index) in new Array(4)"
:key="item"
:style="{ ...baseStyle, background: `${index % 2 ? '#1677ff' : '#1677ffbf'}` }"
/>
</a-flex>
<hr/>
<label>
Select justify:
<select v-model="justify">
<option v-for="item in justifyOptions" :key="item">{{ item }}</option>
</select>
</label>
<label>
Select align:
<select v-model="align">
<option v-for="item in alignOptions" :key="item">{{ item }}</option>
</select>
</label>
<a-flex :style="{ ...boxStyle }" :justify="justify" :align="align">
<a-button variant="solid">Primary</a-button>
<a-button variant="solid">Primary</a-button>
<a-button variant="solid">Primary</a-button>
<a-button variant="solid">Primary</a-button>
</a-flex>
<hr/>
<a-flex gap="middle" vertical>
<label>
Select gap size:
<select v-model="gapSize">
<option v-for="item in gapSizeOptions" :key="item">{{ item }}</option>
</select>
</label>
<a-flex :gap="gapSize">
<a-button variant="solid">Primary</a-button>
<a-button>Default</a-button>
<a-button variant="dashed">Dashed</a-button>
<a-button variant="link">Link</a-button>
</a-flex>
</a-flex>
<hr/>
<label>
Auto wrap:
</label>
<a-flex wrap="wrap" gap="small">
<a-button v-for="item in new Array(24)" :key="item" variant="solid">Button</a-button>
</a-flex>
</a-flex>
</template>
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { ref, reactive } from 'vue';
const baseStyle: CSSProperties = {
width: '25%',
height: '54px',
};
const boxStyle: CSSProperties = {
width: '100%',
height: '120px',
borderRadius: '6px',
border: '1px solid #40a9ff',
};
const axisOptions = reactive(['horizontal', 'vertical']);
const axis = ref(axisOptions[0]);
const justifyOptions = reactive([
'flex-start',
'center',
'flex-end',
'space-between',
'space-around',
'space-evenly',
]);
const justify = ref(justifyOptions[0]);
const alignOptions = reactive(['flex-start', 'center', 'flex-end']);
const align = ref(alignOptions[0]);
const gapSizeOptions = reactive(['small', 'middle', 'large']);
const gapSize = ref(gapSizeOptions[0]);
</script>

View File

@ -0,0 +1,65 @@
import { RouteRecordRaw, RouterView } from 'vue-router'
import BasicLayout from './components/BasicLayout.vue'
import { Fragment, h } from 'vue'
// /pages/button/basic.vue
const items = import.meta.glob('./pages/*/*.vue', { import: 'default', eager: true })
const categoryRoutes: Record<string, RouteRecordRaw[]> = {}
Object.keys(items).forEach(path => {
const route = path.replace('./pages/', '').replace('.vue', '')
const [category, demo] = route.split('/')
if (!categoryRoutes[category]) {
categoryRoutes[category] = []
}
categoryRoutes[category].push({
path: demo,
component: items[path],
})
})
const routes: RouteRecordRaw[] = Object.entries(categoryRoutes).map(([category, children]) => {
const renderComponents = () =>
h(
'div',
children.map(child => h(child.component)),
)
renderComponents.displayName = 'renderComponents'
return {
path: `/${category}`,
component: RouterView,
children: [
...children,
{
path: ':demo*',
component: renderComponents,
},
],
}
})
const navs = Object.keys(categoryRoutes).map(category => ({
name: category,
path: `/${category}`,
children: categoryRoutes[category].map(child => ({
name: child.path,
path: `/${category}/${child.path}`,
})),
}))
routes.push({
path: '/:pathMatch(.*)*',
component: h('div', 'demo not found'),
})
export default [
{
path: '/',
component: BasicLayout,
children: routes,
props: {
navs,
},
},
]

5
apps/playground/src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<Record<string, never>, Record<string, never>, any>
export default component
}

View File

@ -0,0 +1,8 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
declare module 'vue' {
export interface GlobalComponents {
AButton: typeof import('@ant-design-vue/ui').Button
AAffix: typeof import('@ant-design-vue/ui').Affix
}
}
export {}

View File

@ -0,0 +1,99 @@
import { RouteRecordRaw } from 'vue-router'
export function globRoutes(
baseName: string,
globs: Record<string, () => Promise<unknown>>,
): RouteRecordRaw {
const items = Object.entries(globs).map(([path, component]) => {
const match = path.match(/^\.\/pages\/(.+)\/index\.ts$/)
if (!match) {
throw new Error('invalid glob')
}
return {
name: match[1],
component,
}
})
const home: RouteRecordRaw = {
path: '',
components: {
default: () => import('@/components/HomePage.vue'),
breadcrumbs: () => import('@/components/TheBreadcrumbs.vue'),
},
props: {
default: {
items: items.map(item => {
return {
name: item.name,
path: `/${baseName}/${item.name}`,
}
}),
},
breadcrumbs: {
items: [
{
name: 'home',
path: '/',
},
{
name: baseName,
path: `/${baseName}`,
},
],
},
},
meta: {
name: baseName,
title: baseName,
},
}
const pages: RouteRecordRaw[] = items.map(item => {
return {
path: item.name,
components: {
default: item.component,
breadcrumbs: () => import('@/components/TheBreadcrumbs.vue'),
},
props: {
breadcrumbs: {
items: [
{
name: 'home',
path: '/',
},
{
name: baseName,
path: `/${baseName}`,
},
{
name: item.name,
path: `/${baseName}/${item.name}`,
},
],
},
},
meta: {
name: item.name,
title: `${baseName} - ${item.name}`,
},
}
})
return {
path: `/${baseName}`,
component: () => import('@/components/BasicLayout.vue'),
props: {
navs: [
{
name: 'home',
path: '/',
},
],
hideNavbar: true,
hideBreadcrumbs: true,
},
children: [home, ...pages],
} as RouteRecordRaw
}

View File

@ -0,0 +1,11 @@
{
"extends": "@ant-design-vue/typescript-config/tsconfig.vue.json",
"include": ["src/**/*.ts", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }],
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"~/*": ["./assets/*"]
}
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "@ant-design-vue/typescript-config/tsconfig.node.json",
"include": ["vite.config.*"]
}

View File

@ -0,0 +1,20 @@
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'node:path'
import { defineConfig, Plugin } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), tailwindcss()],
server: {
watch: {
ignored: ['!**/node_modules/@ant-design-vue/**'],
},
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'~': resolve(__dirname, './assets'),
},
},
})

View File

@ -1,19 +0,0 @@
module.exports = {
env: {
test: {
presets: [['@babel/preset-env']],
plugins: [
['@vue/babel-plugin-jsx', { mergeProps: false, enableObjectSlots: false }],
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-transform-object-assign',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-runtime',
'transform-require-context',
],
},
},
};

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
rm -rf dist
mkdir dist
./node_modules/.bin/webpack --config webpack.site.config.js
cp dist/index.html index.html

View File

@ -1,11 +1,14 @@
export type KeyType = string | number; export type KeyType = string | number;
type ValueType = [number, any]; // [times, realValue] type ValueType = [number, any]; // [times, realValue]
const SPLIT = '%'; const SPLIT = '%';
class Entity { class Entity {
instanceId: string; instanceId: string;
constructor(instanceId: string) { constructor(instanceId: string) {
this.instanceId = instanceId; this.instanceId = instanceId;
} }
/** @private Internal cache map. Do not access this directly */ /** @private Internal cache map. Do not access this directly */
cache = new Map<string, ValueType>(); cache = new Map<string, ValueType>();

View File

@ -31,7 +31,6 @@ export function createCache() {
Array.from(styles).forEach(style => { Array.from(styles).forEach(style => {
(style as any)[CSS_IN_JS_INSTANCE] = (style as any)[CSS_IN_JS_INSTANCE] || cssinjsInstanceId; (style as any)[CSS_IN_JS_INSTANCE] = (style as any)[CSS_IN_JS_INSTANCE] || cssinjsInstanceId;
// Not force move if no head
// Not force move if no head // Not force move if no head
if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) { if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) {
document.head.insertBefore(style, firstChild); document.head.insertBefore(style, firstChild);

View File

@ -0,0 +1,82 @@
import type Cache from './Cache';
import { extract as tokenExtractStyle, TOKEN_PREFIX } from './hooks/useCacheToken';
import { CSS_VAR_PREFIX, extract as cssVarExtractStyle } from './hooks/useCSSVarRegister';
import { extract as styleExtractStyle, STYLE_PREFIX } from './hooks/useStyleRegister';
import { toStyleStr } from './util';
import { ATTR_CACHE_MAP, serialize as serializeCacheMap } from './util/cacheMapUtil';
const ExtractStyleFns = {
[STYLE_PREFIX]: styleExtractStyle,
[TOKEN_PREFIX]: tokenExtractStyle,
[CSS_VAR_PREFIX]: cssVarExtractStyle,
};
type ExtractStyleType = keyof typeof ExtractStyleFns;
function isNotNull<T>(value: T | null): value is T {
return value !== null;
}
export default function extractStyle(
cache: Cache,
options?:
| boolean
| {
plain?: boolean;
types?: ExtractStyleType | ExtractStyleType[];
},
) {
const { plain = false, types = ['style', 'token', 'cssVar'] } =
typeof options === 'boolean' ? { plain: options } : options || {};
const matchPrefixRegexp = new RegExp(
`^(${(typeof types === 'string' ? [types] : types).join('|')})%`,
);
// prefix with `style` is used for `useStyleRegister` to cache style context
const styleKeys = Array.from(cache.cache.keys()).filter(key => matchPrefixRegexp.test(key));
// Common effect styles like animation
const effectStyles: Record<string, boolean> = {};
// Mapping of cachePath to style hash
const cachePathMap: Record<string, string> = {};
let styleText = '';
styleKeys
.map<[number, string] | null>(key => {
const cachePath = key.replace(matchPrefixRegexp, '').replace(/%/g, '|');
const [prefix] = key.split('%');
const extractFn = ExtractStyleFns[prefix as keyof typeof ExtractStyleFns];
const extractedStyle = extractFn(cache.cache.get(key)![1], effectStyles, {
plain,
});
if (!extractedStyle) {
return null;
}
const [order, styleId, styleStr] = extractedStyle;
if (key.startsWith('style')) {
cachePathMap[cachePath] = styleId;
}
return [order, styleStr];
})
.filter(isNotNull)
.sort(([o1], [o2]) => o1 - o2)
.forEach(([, style]) => {
styleText += style;
});
// ==================== Fill Cache Path ====================
styleText += toStyleStr(
`.${ATTR_CACHE_MAP}{content:"${serializeCacheMap(cachePathMap)}";}`,
undefined,
undefined,
{
[ATTR_CACHE_MAP]: ATTR_CACHE_MAP,
},
plain,
);
return styleText;
}

View File

@ -0,0 +1,108 @@
import { removeCSS, updateCSS } from '../../../vc-util/Dom/dynamicCSS';
import { ATTR_MARK, ATTR_TOKEN, CSS_IN_JS_INSTANCE, useStyleInject } from '../StyleContext';
import { isClientSide, toStyleStr } from '../util';
import type { TokenWithCSSVar } from '../util/css-variables';
import { transformToken } from '../util/css-variables';
import type { ExtractStyle } from './useGlobalCache';
import useGlobalCache from './useGlobalCache';
import { uniqueHash } from './useStyleRegister';
import type { ComputedRef } from 'vue';
import { computed } from 'vue';
export const CSS_VAR_PREFIX = 'cssVar';
type CSSVarCacheValue<V, T extends Record<string, V> = Record<string, V>> = [
cssVarToken: TokenWithCSSVar<V, T>,
cssVarStr: string,
styleId: string,
cssVarKey: string,
];
const useCSSVarRegister = <V, T extends Record<string, V>>(
config: ComputedRef<{
path: string[];
key: string;
prefix?: string;
unitless?: Record<string, boolean>;
ignore?: Record<string, boolean>;
scope?: string;
token: any;
}>,
fn: () => T,
) => {
const styleContext = useStyleInject();
const stylePath = computed(() => {
return [
...config.value.path,
config.value.key,
config.value.scope || '',
config.value.token?._tokenKey,
];
});
const cache = useGlobalCache<CSSVarCacheValue<V, T>>(
CSS_VAR_PREFIX,
stylePath,
() => {
const originToken = fn();
const [mergedToken, cssVarsStr] = transformToken<V, T>(originToken, config.value.key, {
prefix: config.value.prefix,
unitless: config.value.unitless,
ignore: config.value.ignore,
scope: config.value.scope || '',
});
const styleId = uniqueHash(stylePath.value, cssVarsStr);
return [mergedToken, cssVarsStr, styleId, config.value.key];
},
([, , styleId]) => {
if (isClientSide) {
removeCSS(styleId, { mark: ATTR_MARK });
}
},
([, cssVarsStr, styleId]) => {
if (!cssVarsStr) {
return;
}
const style = updateCSS(cssVarsStr, styleId, {
mark: ATTR_MARK,
prepend: 'queue',
attachTo: styleContext.value.container,
priority: -999,
});
(style as any)[CSS_IN_JS_INSTANCE] = styleContext.value.cache?.instanceId;
// Used for `useCacheToken` to remove on batch when token removed
style.setAttribute(ATTR_TOKEN, config.value.key);
},
);
return cache;
};
export const extract: ExtractStyle<CSSVarCacheValue<any>> = (cache, _effectStyles, options) => {
const [, styleStr, styleId, cssVarKey] = cache;
const { plain } = options || {};
if (!styleStr) {
return null;
}
const order = -999;
// ====================== Style ======================
// Used for rc-util
const sharedAttrs = {
'data-vc-order': 'prependQueue',
'data-vc-priority': `${order}`,
};
const styleText = toStyleStr(styleStr, cssVarKey, styleId, sharedAttrs, plain);
return [order, styleId, styleText];
};
export default useCSSVarRegister;

View File

@ -1,20 +1,19 @@
import hash from '@emotion/hash'; import hash from '@emotion/hash';
import { ATTR_TOKEN, CSS_IN_JS_INSTANCE, useStyleInject } from '../StyleContext'; import { updateCSS } from '../../../vc-util/Dom/dynamicCSS';
import { ATTR_MARK, ATTR_TOKEN, CSS_IN_JS_INSTANCE, useStyleInject } from '../StyleContext';
import type Theme from '../theme/Theme'; import type Theme from '../theme/Theme';
import { flattenToken, memoResult, token2key, toStyleStr } from '../util';
import { transformToken } from '../util/css-variables';
import type { ExtractStyle } from './useGlobalCache';
import useGlobalCache from './useGlobalCache'; import useGlobalCache from './useGlobalCache';
import { flattenToken, token2key } from '../util';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
const EMPTY_OVERRIDE = {}; const EMPTY_OVERRIDE = {};
const isProduction = process.env.NODE_ENV === 'production';
// nuxt generate when NODE_ENV is prerender
const isPrerender = process.env.NODE_ENV === 'prerender';
// Generate different prefix to make user selector break in production env. // Generate different prefix to make user selector break in production env.
// This helps developer not to do style override directly on the hash id. // This helps developer not to do style override directly on the hash id.
const hashPrefix = !isProduction && !isPrerender ? 'css-dev-only-do-not-override' : 'css'; const hashPrefix = process.env.NODE_ENV !== 'production' ? 'css-dev-only-do-not-override' : 'css';
export interface Option<DerivativeToken, DesignToken> { export interface Option<DerivativeToken, DesignToken> {
/** /**
@ -46,6 +45,22 @@ export interface Option<DerivativeToken, DesignToken> {
override: object, override: object,
theme: Theme<any, any>, theme: Theme<any, any>,
) => DerivativeToken; ) => DerivativeToken;
/**
* Transform token to css variables.
*/
cssVar?: {
/** Prefix for css variables */
prefix?: string;
/** Tokens that should not be appended with unit */
unitless?: Record<string, boolean>;
/** Tokens that should not be transformed to css variables */
ignore?: Record<string, boolean>;
/** Tokens that preserves origin value */
preserve?: Record<string, boolean>;
/** Key for current theme. Useful for customizing and should be unique */
key?: string;
};
} }
const tokenKeys = new Map<string, number>(); const tokenKeys = new Map<string, number>();
@ -94,6 +109,7 @@ export const getComputedToken = <DerivativeToken = object, DesignToken = Derivat
format?: (token: DesignToken) => DerivativeToken, format?: (token: DesignToken) => DerivativeToken,
) => { ) => {
const derivativeToken = theme.getDerivativeToken(originToken); const derivativeToken = theme.getDerivativeToken(originToken);
// Merge with override // Merge with override
let mergedDerivativeToken = { let mergedDerivativeToken = {
...derivativeToken, ...derivativeToken,
@ -108,6 +124,16 @@ export const getComputedToken = <DerivativeToken = object, DesignToken = Derivat
return mergedDerivativeToken; return mergedDerivativeToken;
}; };
export const TOKEN_PREFIX = 'token';
type TokenCacheValue<DerivativeToken> = [
token: DerivativeToken & { _tokenKey: string; _themeKey: string },
hashId: string,
realToken: DerivativeToken & { _tokenKey: string },
cssVarStr: string,
cssVarKey: string,
];
/** /**
* Cache theme derivative token as global shared one * Cache theme derivative token as global shared one
* @param theme Theme entity * @param theme Theme entity
@ -119,21 +145,27 @@ export default function useCacheToken<DerivativeToken = object, DesignToken = De
theme: Ref<Theme<any, any>>, theme: Ref<Theme<any, any>>,
tokens: Ref<Partial<DesignToken>[]>, tokens: Ref<Partial<DesignToken>[]>,
option: Ref<Option<DerivativeToken, DesignToken>> = ref({}), option: Ref<Option<DerivativeToken, DesignToken>> = ref({}),
) { ): Ref<TokenCacheValue<DerivativeToken>> {
const style = useStyleInject(); const styleContext = useStyleInject();
// Basic - We do basic cache here // Basic - We do basic cache here
const mergedToken = computed(() => Object.assign({}, ...tokens.value)); const mergedToken = computed(() =>
const tokenStr = computed(() => flattenToken(mergedToken.value)); memoResult(() => Object.assign({}, ...tokens.value), tokens.value),
const overrideTokenStr = computed(() => flattenToken(option.value.override || EMPTY_OVERRIDE)); );
const cachedToken = useGlobalCache<[DerivativeToken & { _tokenKey: string }, string]>( const tokenStr = computed(() => flattenToken(mergedToken.value));
'token', const overrideTokenStr = computed(() => flattenToken(option.value.override ?? EMPTY_OVERRIDE));
const cssVarStr = computed(() => (option.value.cssVar ? flattenToken(option.value.cssVar) : ''));
const cachedToken = useGlobalCache<TokenCacheValue<DerivativeToken>>(
TOKEN_PREFIX,
computed(() => [ computed(() => [
option.value.salt || '', option.value.salt ?? '',
theme.value.id, theme.value?.id,
tokenStr.value, tokenStr.value,
overrideTokenStr.value, overrideTokenStr.value,
cssVarStr.value,
]), ]),
() => { () => {
const { const {
@ -141,25 +173,82 @@ export default function useCacheToken<DerivativeToken = object, DesignToken = De
override = EMPTY_OVERRIDE, override = EMPTY_OVERRIDE,
formatToken, formatToken,
getComputedToken: compute, getComputedToken: compute,
cssVar,
} = option.value; } = option.value;
const mergedDerivativeToken = compute let mergedDerivativeToken = compute
? compute(mergedToken.value, override, theme.value) ? compute(mergedToken.value, override, theme.value)
: getComputedToken(mergedToken.value, override, theme.value, formatToken); : getComputedToken(mergedToken.value, override, theme.value, formatToken);
// Replace token value with css variables
const actualToken = { ...mergedDerivativeToken };
let cssVarsStr = '';
if (!!cssVar) {
[mergedDerivativeToken, cssVarsStr] = transformToken(mergedDerivativeToken, cssVar.key!, {
prefix: cssVar.prefix,
ignore: cssVar.ignore,
unitless: cssVar.unitless,
preserve: cssVar.preserve,
});
}
// Optimize for `useStyleRegister` performance // Optimize for `useStyleRegister` performance
const tokenKey = token2key(mergedDerivativeToken, salt); const tokenKey = token2key(mergedDerivativeToken, salt);
mergedDerivativeToken._tokenKey = tokenKey; mergedDerivativeToken._tokenKey = tokenKey;
recordCleanToken(tokenKey); actualToken._tokenKey = token2key(actualToken, salt);
const themeKey = cssVar?.key ?? tokenKey;
mergedDerivativeToken._themeKey = themeKey;
recordCleanToken(themeKey);
const hashId = `${hashPrefix}-${hash(tokenKey)}`; const hashId = `${hashPrefix}-${hash(tokenKey)}`;
mergedDerivativeToken._hashId = hashId; // Not used mergedDerivativeToken._hashId = hashId; // Not used
return [mergedDerivativeToken, hashId];
return [mergedDerivativeToken, hashId, actualToken, cssVarsStr, cssVar?.key || ''];
}, },
cache => { cache => {
// Remove token will remove all related style // Remove token will remove all related style
cleanTokenStyle(cache[0]._tokenKey, style.value?.cache.instanceId); cleanTokenStyle(cache[0]._themeKey, styleContext.value?.cache?.instanceId);
},
([token, , , cssVarsStr]) => {
const { cssVar } = option.value;
if (cssVar && cssVarsStr) {
const style = updateCSS(cssVarsStr, hash(`css-variables-${token._themeKey}`), {
mark: ATTR_MARK,
prepend: 'queue',
attachTo: styleContext.value?.container,
priority: -999,
});
(style as any)[CSS_IN_JS_INSTANCE] = styleContext.value?.cache?.instanceId;
// Used for `useCacheToken` to remove on batch when token removed
style.setAttribute(ATTR_TOKEN, token._themeKey);
}
}, },
); );
return cachedToken; return cachedToken;
} }
export const extract: ExtractStyle<TokenCacheValue<any>> = (cache, _effectStyles, options) => {
const [, , realToken, styleStr, cssVarKey] = cache;
const { plain } = options || {};
if (!styleStr) {
return null;
}
const styleId = realToken._tokenKey;
const order = -999;
// ====================== Style ======================
// Used for rc-util
const sharedAttrs = {
'data-vc-order': 'prependQueue',
'data-vc-priority': `${order}`,
};
const styleText = toStyleStr(styleStr, cssVarKey, styleId, sharedAttrs, plain);
return [order, styleId, styleText];
};

View File

@ -0,0 +1,30 @@
// import canUseDom from 'rc-util/lib/Dom/canUseDom';
import useLayoutEffect from '../../../_util/hooks/useLayoutEffect';
import type { ShallowRef, WatchCallback } from 'vue';
import { watch } from 'vue';
type UseCompatibleInsertionEffect = (
renderEffect: WatchCallback,
effect: (polyfill?: boolean) => ReturnType<WatchCallback>,
deps: ShallowRef,
) => void;
/**
* Polyfill `useInsertionEffect` for React < 18
* @param renderEffect will be executed in `useMemo`, and do not have callback
* @param effect will be executed in `useLayoutEffect`
* @param deps
*/
const useInsertionEffectPolyfill: UseCompatibleInsertionEffect = (renderEffect, effect, deps) => {
watch(deps, renderEffect, { immediate: true });
useLayoutEffect(() => effect(true), deps);
};
/**
* Compatible `useInsertionEffect`
* will use `useInsertionEffect` if React version >= 18,
* otherwise use `useInsertionEffectPolyfill`.
*/
const useCompatibleInsertionEffect: UseCompatibleInsertionEffect = useInsertionEffectPolyfill;
export default useCompatibleInsertionEffect;

View File

@ -0,0 +1,8 @@
const useRun = () => {
return function (fn: () => void) {
fn();
};
};
const useEffectCleanupRegister = useRun;
export default useEffectCleanupRegister;

View File

@ -1,58 +1,115 @@
import { useStyleInject } from '../StyleContext'; import { useStyleInject } from '../StyleContext';
import type { KeyType } from '../Cache'; import type { KeyType } from '../Cache';
import useCompatibleInsertionEffect from './useCompatibleInsertionEffect';
import useHMR from './useHMR'; import useHMR from './useHMR';
import type { ShallowRef, Ref } from 'vue'; import type { ShallowRef, Ref } from 'vue';
import { onBeforeUnmount, watch, watchEffect, shallowRef } from 'vue'; import { onBeforeUnmount, watch, computed } from 'vue';
export default function useClientCache<CacheType>(
export type ExtractStyle<CacheValue> = (
cache: CacheValue,
effectStyles: Record<string, boolean>,
options?: {
plain?: boolean;
},
) => [order: number, styleId: string, style: string] | null;
export default function useGlobalCache<CacheType>(
prefix: string, prefix: string,
keyPath: Ref<KeyType[]>, keyPath: Ref<KeyType[]>,
cacheFn: () => CacheType, cacheFn: () => CacheType,
onCacheRemove?: (cache: CacheType, fromHMR: boolean) => void, onCacheRemove?: (cache: CacheType, fromHMR: boolean) => void,
// Add additional effect trigger by `useInsertionEffect`
onCacheEffect?: (cachedValue: CacheType) => void,
): ShallowRef<CacheType> { ): ShallowRef<CacheType> {
const styleContext = useStyleInject(); const styleContext = useStyleInject();
const fullPathStr = shallowRef(''); const globalCache = computed(() => styleContext.value?.cache);
const res = shallowRef<CacheType>(); const deps = computed(() => [prefix, ...keyPath.value].join('%'));
watchEffect(() => {
fullPathStr.value = [prefix, ...keyPath.value].join('%');
});
const HMRUpdate = useHMR(); const HMRUpdate = useHMR();
const clearCache = (pathStr: string) => {
styleContext.value.cache.update(pathStr, prevCache => { type UpdaterArgs = [times: number, cache: CacheType];
const [times = 0, cache] = prevCache || [];
const nextCount = times - 1; const buildCache = (updater?: (data: UpdaterArgs) => UpdaterArgs) => {
if (nextCount === 0) { globalCache.value.update(deps.value, prevCache => {
onCacheRemove?.(cache, false); const [times = 0, cache] = prevCache || [undefined, undefined];
return null;
// HMR should always ignore cache since developer may change it
let tmpCache = cache;
if (process.env.NODE_ENV !== 'production' && cache && HMRUpdate) {
onCacheRemove?.(tmpCache, HMRUpdate);
tmpCache = null;
} }
return [times - 1, cache]; const mergedCache = tmpCache || cacheFn();
const data: UpdaterArgs = [times, mergedCache];
// Call updater if need additional logic
return updater ? updater(data) : data;
}); });
}; };
watch( watch(
fullPathStr, deps,
(newStr, oldStr) => { () => {
if (oldStr) clearCache(oldStr); buildCache();
// Create cache
styleContext.value.cache.update(newStr, prevCache => {
const [times = 0, cache] = prevCache || [];
// HMR should always ignore cache since developer may change it
let tmpCache = cache;
if (process.env.NODE_ENV !== 'production' && cache && HMRUpdate) {
onCacheRemove?.(tmpCache, HMRUpdate);
tmpCache = null;
}
const mergedCache = tmpCache || cacheFn();
return [times + 1, mergedCache];
});
res.value = styleContext.value.cache.get(fullPathStr.value)![1];
}, },
{ immediate: true }, { immediate: true },
); );
let cacheEntity = globalCache.value.get(deps.value);
// HMR clean the cache but not trigger `useMemo` again
// Let's fallback of this
// ref https://github.com/ant-design/cssinjs/issues/127
if (process.env.NODE_ENV !== 'production' && !cacheEntity) {
buildCache();
cacheEntity = globalCache.value.get(deps.value);
}
const cacheContent = computed(
() =>
(globalCache.value.get(deps.value) && globalCache.value.get(deps.value)![1]) ||
cacheEntity![1],
);
// Remove if no need anymore
useCompatibleInsertionEffect(
() => {
onCacheEffect?.(cacheContent.value);
},
polyfill => {
// It's bad to call build again in effect.
// But we have to do this since StrictMode will call effect twice
// which will clear cache on the first time.
buildCache(([times, cache]) => {
if (polyfill && times === 0) {
onCacheEffect?.(cacheContent.value);
}
return [times + 1, cache];
});
return () => {
globalCache.value.update(deps.value, prevCache => {
const [times = 0, cache] = prevCache || [];
const nextCount = times - 1;
if (nextCount <= 0) {
if (polyfill || !globalCache.value.get(deps.value)) {
onCacheRemove?.(cache, false);
}
return null;
}
return [times - 1, cache];
});
};
},
deps,
);
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearCache(fullPathStr.value); buildCache();
}); });
return res;
return cacheContent;
} }

View File

@ -3,38 +3,30 @@ import type * as CSS from 'csstype';
// @ts-ignore // @ts-ignore
import unitless from '@emotion/unitless'; import unitless from '@emotion/unitless';
import { compile, serialize, stringify } from 'stylis'; import { compile, serialize, stringify } from 'stylis';
import type { Theme, Transformer } from '../..'; import type { Theme, Transformer } from '..';
import type Cache from '../../Cache'; import type Keyframes from '../Keyframes';
import type Keyframes from '../../Keyframes'; import type { Linter } from '../linters';
import type { Linter } from '../../linters'; import { contentQuotesLinter, hashedAnimationLinter } from '../linters';
import { contentQuotesLinter, hashedAnimationLinter } from '../../linters'; import type { HashPriority } from '../StyleContext';
import type { HashPriority } from '../../StyleContext';
import { import {
useStyleInject, useStyleInject,
ATTR_CACHE_PATH, ATTR_CACHE_PATH,
ATTR_MARK, ATTR_MARK,
ATTR_TOKEN, ATTR_TOKEN,
CSS_IN_JS_INSTANCE, CSS_IN_JS_INSTANCE,
} from '../../StyleContext'; } from '../StyleContext';
import { supportLayer } from '../../util'; import { isClientSide, supportLayer, toStyleStr } from '../util';
import useGlobalCache from '../useGlobalCache'; import { CSS_FILE_STYLE, existPath, getStyleAndHash } from '../util/cacheMapUtil';
import { removeCSS, updateCSS } from '../../../../vc-util/Dom/dynamicCSS'; import type { ExtractStyle } from './useGlobalCache';
import useGlobalCache from './useGlobalCache';
import { removeCSS, updateCSS } from '../../../vc-util/Dom/dynamicCSS';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed } from 'vue'; import { computed } from 'vue';
import type { VueNode } from '../../../type'; import type { VueNode } from '../../type';
import canUseDom from '../../../../_util/canUseDom';
import {
ATTR_CACHE_MAP,
existPath,
getStyleAndHash,
serialize as serializeCacheMap,
} from './cacheMapUtil';
const isClientSide = canUseDom();
const SKIP_CHECK = '_skip_check_'; const SKIP_CHECK = '_skip_check_';
const MULTI_VALUE = '_multi_value_'; const MULTI_VALUE = '_multi_value_';
export type CSSProperties = Omit<CSS.PropertiesFallback<number | string>, 'animationName'> & { export type CSSProperties = Omit<CSS.PropertiesFallback<number | string>, 'animationName'> & {
animationName?: CSS.PropertiesFallback<number | string>['animationName'] | Keyframes; animationName?: CSS.PropertiesFallback<number | string>['animationName'] | Keyframes;
}; };
@ -60,6 +52,7 @@ export type CSSInterpolation = InterpolationPrimitive | ArrayCSSInterpolation |
export type CSSOthersObject = Record<string, CSSInterpolation>; export type CSSOthersObject = Record<string, CSSInterpolation>;
// @ts-ignore
export interface CSSObject extends CSSPropertiesWithMultiValues, CSSPseudos, CSSOthersObject {} export interface CSSObject extends CSSPropertiesWithMultiValues, CSSPseudos, CSSOthersObject {}
// ============================================================================ // ============================================================================
@ -114,16 +107,6 @@ export interface ParseInfo {
parentSelectors: string[]; parentSelectors: string[];
} }
// Global effect style will mount once and not removed
// The effect will not save in SSR cache (e.g. keyframes)
const globalEffectStyleKeys = new Set();
/**
* @private Test only. Clear the global effect style keys.
*/
export const _cf =
process.env.NODE_ENV !== 'production' ? () => globalEffectStyleKeys.clear() : undefined;
// Parse CSSObject to style content // Parse CSSObject to style content
export const parseStyle = ( export const parseStyle = (
interpolation: CSSInterpolation, interpolation: CSSInterpolation,
@ -258,6 +241,7 @@ export const parseStyle = (
styleStr += `${styleName}:${formatValue};`; styleStr += `${styleName}:${formatValue};`;
} }
const actualValue = (value as any)?.value ?? value; const actualValue = (value as any)?.value ?? value;
if ( if (
typeof value === 'object' && typeof value === 'object' &&
@ -295,7 +279,7 @@ export const parseStyle = (
// ============================================================================ // ============================================================================
// == Register == // == Register ==
// ============================================================================ // ============================================================================
function uniqueHash(path: (string | number)[], styleStr: string) { export function uniqueHash(path: (string | number)[], styleStr: string) {
return hash(`${path.join('%')}${styleStr}`); return hash(`${path.join('%')}${styleStr}`);
} }
@ -303,6 +287,17 @@ function uniqueHash(path: (string | number)[], styleStr: string) {
// return null; // return null;
// } // }
export const STYLE_PREFIX = 'style';
type StyleCacheValue = [
styleStr: string,
tokenKey: string,
styleId: string,
effectStyle: Record<string, string>,
clientOnly: boolean | undefined,
order: number,
];
/** /**
* Register a style to the global style sheet. * Register a style to the global style sheet.
*/ */
@ -337,22 +332,14 @@ export default function useStyleRegister(
} }
// const [cacheStyle[0], cacheStyle[1], cacheStyle[2]] // const [cacheStyle[0], cacheStyle[1], cacheStyle[2]]
useGlobalCache< useGlobalCache<StyleCacheValue>(
[ STYLE_PREFIX,
styleStr: string,
tokenKey: string,
styleId: string,
effectStyle: Record<string, string>,
clientOnly: boolean | undefined,
order: number,
]
>(
'style',
fullPath, fullPath,
// Create cache if needed // Create cache if needed
() => { () => {
const { path, hashId, layer, nonce, clientOnly, order = 0 } = info.value; const { path, hashId, layer, clientOnly, order = 0 } = info.value;
const cachePath = fullPath.value.join('|'); const cachePath = fullPath.value.join('|');
// Get style from SSR inline style directly // Get style from SSR inline style directly
if (existPath(cachePath)) { if (existPath(cachePath)) {
const [inlineCacheStyleStr, styleHash] = getStyleAndHash(cachePath); const [inlineCacheStyleStr, styleHash] = getStyleAndHash(cachePath);
@ -360,8 +347,10 @@ export default function useStyleRegister(
return [inlineCacheStyleStr, tokenKey.value, styleHash, {}, clientOnly, order]; return [inlineCacheStyleStr, tokenKey.value, styleHash, {}, clientOnly, order];
} }
} }
// Generate style
const styleObj = styleFn(); const styleObj = styleFn();
const { hashPriority, container, transformers, linters, cache } = styleContext.value; const { hashPriority, transformers, linters } = styleContext.value;
const [parsedStyle, effectStyle] = parseStyle(styleObj, { const [parsedStyle, effectStyle] = parseStyle(styleObj, {
hashId, hashId,
@ -371,18 +360,32 @@ export default function useStyleRegister(
transformers, transformers,
linters, linters,
}); });
const styleStr = normalizeStyle(parsedStyle); const styleStr = normalizeStyle(parsedStyle);
const styleId = uniqueHash(fullPath.value, styleStr); const styleId = uniqueHash(fullPath.value, styleStr);
if (isMergedClientSide) { return [styleStr, tokenKey.value, styleId, effectStyle, clientOnly, order];
},
// Remove cache if no need
([, , styleId], fromHMR) => {
if ((fromHMR || styleContext.value.autoClear) && isClientSide) {
removeCSS(styleId, { mark: ATTR_MARK });
}
},
// Effect: Inject style here
([styleStr, , styleId, effectStyle]) => {
if (isMergedClientSide && styleStr !== CSS_FILE_STYLE) {
const mergedCSSConfig: Parameters<typeof updateCSS>[2] = { const mergedCSSConfig: Parameters<typeof updateCSS>[2] = {
mark: ATTR_MARK, mark: ATTR_MARK,
prepend: 'queue', prepend: 'queue',
attachTo: container, attachTo: styleContext.value.container,
priority: order, priority: info.value.order,
}; };
const nonceStr = typeof nonce === 'function' ? nonce() : nonce; const nonceStr =
typeof info.value.nonce === 'function' ? info.value.nonce() : info.value.nonce;
if (nonceStr) { if (nonceStr) {
mergedCSSConfig.csp = { nonce: nonceStr }; mergedCSSConfig.csp = { nonce: nonceStr };
@ -390,45 +393,33 @@ export default function useStyleRegister(
const style = updateCSS(styleStr, styleId, mergedCSSConfig); const style = updateCSS(styleStr, styleId, mergedCSSConfig);
(style as any)[CSS_IN_JS_INSTANCE] = cache.instanceId; (style as any)[CSS_IN_JS_INSTANCE] = styleContext.value.cache.instanceId;
// Used for `useCacheToken` to remove on batch when token removed // Used for `useCacheToken` to remove on batch when token removed
style.setAttribute(ATTR_TOKEN, tokenKey.value); style.setAttribute(ATTR_TOKEN, tokenKey.value);
// Dev usage to find which cache path made this easily // Debug usage. Dev only
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
style.setAttribute(ATTR_CACHE_PATH, fullPath.value.join('|')); style.setAttribute(ATTR_CACHE_PATH, fullPath.value.join('|'));
} }
// Inject client side effect style // Inject client side effect style
Object.keys(effectStyle).forEach(effectKey => { Object.keys(effectStyle).forEach(effectKey => {
if (!globalEffectStyleKeys.has(effectKey)) { updateCSS(
globalEffectStyleKeys.add(effectKey); normalizeStyle(effectStyle[effectKey]),
`_effect-${effectKey}`,
// Inject mergedCSSConfig,
updateCSS(normalizeStyle(effectStyle[effectKey]), `_effect-${effectKey}`, { );
mark: ATTR_MARK,
prepend: 'queue',
attachTo: container,
});
}
}); });
} }
return [styleStr, tokenKey.value, styleId, effectStyle, clientOnly, order];
},
// Remove cache if no need
([, , styleId], fromHMR) => {
if ((fromHMR || styleContext.value.autoClear) && isClientSide) {
removeCSS(styleId, { mark: ATTR_MARK });
}
}, },
); );
return (node: VueNode) => { return (node: VueNode) => {
return node; return node;
// let styleNode: VueNode; // let styleNode: VueNode;
// if (!styleContext.ssrInline || isMergedClientSide || !styleContext.defaultCache) {
// if (!styleContext.value.ssrInline || isMergedClientSide || !styleContext.value.defaultCache) {
// styleNode = <Empty />; // styleNode = <Empty />;
// } else { // } else {
// styleNode = ( // styleNode = (
@ -451,116 +442,43 @@ export default function useStyleRegister(
}; };
} }
// ============================================================================ export const extract: ExtractStyle<StyleCacheValue> = (cache, effectStyles, options) => {
// == SSR == const [styleStr, tokenKey, styleId, effectStyle, clientOnly, order]: StyleCacheValue = cache;
// ============================================================================ const { plain } = options || {};
export function extractStyle(cache: Cache, plain = false) {
const matchPrefix = `style%`;
// prefix with `style` is used for `useStyleRegister` to cache style context // Skip client only style
const styleKeys = Array.from(cache.cache.keys()).filter(key => key.startsWith(matchPrefix)); if (clientOnly) {
return null;
// Common effect styles like animation
const effectStyles: Record<string, boolean> = {};
// Mapping of cachePath to style hash
const cachePathMap: Record<string, string> = {};
let styleText = '';
function toStyleStr(
style: string,
tokenKey?: string,
styleId?: string,
customizeAttrs: Record<string, string> = {},
) {
const attrs: Record<string, string | undefined> = {
...customizeAttrs,
[ATTR_TOKEN]: tokenKey,
[ATTR_MARK]: styleId,
};
const attrStr = Object.keys(attrs)
.map(attr => {
const val = attrs[attr];
return val ? `${attr}="${val}"` : null;
})
.filter(v => v)
.join(' ');
return plain ? style : `<style ${attrStr}>${style}</style>`;
} }
// ====================== Fill Style ====================== let keyStyleText = styleStr;
type OrderStyle = [order: number, style: string];
const orderStyles: OrderStyle[] = styleKeys // ====================== Style ======================
.map(key => { // Used for rc-util
const cachePath = key.slice(matchPrefix.length).replace(/%/g, '|'); const sharedAttrs = {
'data-vc-order': 'prependQueue',
'data-vc-priority': `${order}`,
};
const [styleStr, tokenKey, styleId, effectStyle, clientOnly, order]: [ keyStyleText = toStyleStr(styleStr, tokenKey, styleId, sharedAttrs, plain);
string,
string,
string,
Record<string, string>,
boolean,
number,
] = cache.cache.get(key)![1];
// Skip client only style // =============== Create effect style ===============
if (clientOnly) { if (effectStyle) {
return null! as OrderStyle; Object.keys(effectStyle).forEach(effectKey => {
// Effect style can be reused
if (!effectStyles[effectKey]) {
effectStyles[effectKey] = true;
const effectStyleStr = normalizeStyle(effectStyle[effectKey]);
keyStyleText += toStyleStr(
effectStyleStr,
tokenKey,
`_effect-${effectKey}`,
sharedAttrs,
plain,
);
} }
// ====================== Style ======================
// Used for vc-util
const sharedAttrs = {
'data-vc-order': 'prependQueue',
'data-vc-priority': `${order}`,
};
let keyStyleText = toStyleStr(styleStr, tokenKey, styleId, sharedAttrs);
// Save cache path with hash mapping
cachePathMap[cachePath] = styleId;
// =============== Create effect style ===============
if (effectStyle) {
Object.keys(effectStyle).forEach(effectKey => {
// Effect style can be reused
if (!effectStyles[effectKey]) {
effectStyles[effectKey] = true;
keyStyleText += toStyleStr(
normalizeStyle(effectStyle[effectKey]),
tokenKey,
`_effect-${effectKey}`,
sharedAttrs,
);
}
});
}
const ret: OrderStyle = [order, keyStyleText];
return ret;
})
.filter(o => o);
orderStyles
.sort((o1, o2) => o1[0] - o2[0])
.forEach(([, style]) => {
styleText += style;
}); });
}
// ==================== Fill Cache Path ==================== return [order, styleId, keyStyleText];
styleText += toStyleStr( };
`.${ATTR_CACHE_MAP}{content:"${serializeCacheMap(cachePathMap)}";}`,
undefined,
undefined,
{
[ATTR_CACHE_MAP]: ATTR_CACHE_MAP,
},
);
return styleText;
}

View File

@ -1,51 +1,37 @@
import useCacheToken from './hooks/useCacheToken'; import extractStyle from './extractStyle';
import useCacheToken, { getComputedToken } from './hooks/useCacheToken';
import useCSSVarRegister from './hooks/useCSSVarRegister';
import type { CSSInterpolation, CSSObject } from './hooks/useStyleRegister'; import type { CSSInterpolation, CSSObject } from './hooks/useStyleRegister';
import useStyleRegister, { extractStyle } from './hooks/useStyleRegister'; import useStyleRegister from './hooks/useStyleRegister';
import Keyframes from './Keyframes'; import Keyframes from './Keyframes';
import type { Linter } from './linters'; import type { Linter } from './linters';
import { legacyNotSelectorLinter, logicalPropertiesLinter, parentSelectorLinter } from './linters'; import {
import type { StyleContextProps, StyleProviderProps } from './StyleContext'; legacyNotSelectorLinter,
import { createCache, useStyleInject, useStyleProvider, StyleProvider } from './StyleContext'; logicalPropertiesLinter,
NaNLinter,
parentSelectorLinter,
} from './linters';
import type { StyleProviderProps } from './StyleContext';
import { createCache, StyleProvider } from './StyleContext';
import type { DerivativeFunc, TokenType } from './theme'; import type { DerivativeFunc, TokenType } from './theme';
import { createTheme, Theme } from './theme'; import { createTheme, Theme } from './theme';
import type { Transformer } from './transformers/interface'; import type { Transformer } from './transformers/interface';
import legacyLogicalPropertiesTransformer from './transformers/legacyLogicalProperties'; import legacyLogicalPropertiesTransformer from './transformers/legacyLogicalProperties';
import px2remTransformer from './transformers/px2rem'; import px2remTransformer from './transformers/px2rem';
import { supportLogicProps, supportWhere } from './util'; import { supportLogicProps, supportWhere, unit } from './util';
import { token2CSSVar } from './util/css-variables';
const cssinjs = {
Theme,
createTheme,
useStyleRegister,
useCacheToken,
createCache,
useStyleInject,
useStyleProvider,
Keyframes,
extractStyle,
// Transformer
legacyLogicalPropertiesTransformer,
px2remTransformer,
// Linters
logicalPropertiesLinter,
legacyNotSelectorLinter,
parentSelectorLinter,
// cssinjs
StyleProvider,
};
export { export {
Theme, Theme,
createTheme, createTheme,
useStyleRegister, useStyleRegister,
useCSSVarRegister,
useCacheToken, useCacheToken,
createCache, createCache,
useStyleInject, StyleProvider,
useStyleProvider,
Keyframes, Keyframes,
extractStyle, extractStyle,
getComputedToken,
// Transformer // Transformer
legacyLogicalPropertiesTransformer, legacyLogicalPropertiesTransformer,
@ -55,9 +41,11 @@ export {
logicalPropertiesLinter, logicalPropertiesLinter,
legacyNotSelectorLinter, legacyNotSelectorLinter,
parentSelectorLinter, parentSelectorLinter,
NaNLinter,
// cssinjs // util
StyleProvider, token2CSSVar,
unit,
}; };
export type { export type {
TokenType, TokenType,
@ -66,12 +54,9 @@ export type {
DerivativeFunc, DerivativeFunc,
Transformer, Transformer,
Linter, Linter,
StyleContextProps,
StyleProviderProps, StyleProviderProps,
}; };
export const _experimental = { export const _experimental = {
supportModernCSS: () => supportWhere() && supportLogicProps(), supportModernCSS: () => supportWhere() && supportLogicProps(),
}; };
export default cssinjs;

View File

@ -0,0 +1,10 @@
import type { Linter } from './interface';
import { lintWarning } from './utils';
const linter: Linter = (key, value, info) => {
if ((typeof value === 'string' && /NaN/g.test(value)) || Number.isNaN(value)) {
lintWarning(`Unexpected 'NaN' in property '${key}: ${value}'.`, info);
}
};
export default linter;

View File

@ -3,4 +3,5 @@ export { default as hashedAnimationLinter } from './hashedAnimationLinter';
export type { Linter } from './interface'; export type { Linter } from './interface';
export { default as legacyNotSelectorLinter } from './legacyNotSelectorLinter'; export { default as legacyNotSelectorLinter } from './legacyNotSelectorLinter';
export { default as logicalPropertiesLinter } from './logicalPropertiesLinter'; export { default as logicalPropertiesLinter } from './logicalPropertiesLinter';
export { default as NaNLinter } from './NaNLinter';
export { default as parentSelectorLinter } from './parentSelectorLinter'; export { default as parentSelectorLinter } from './parentSelectorLinter';

View File

@ -6,8 +6,8 @@ export function lintWarning(message: string, info: LinterInfo) {
devWarning( devWarning(
false, false,
`[Ant Design Vue CSS-in-JS] ${path ? `Error in '${path}': ` : ''}${message}${ `[Ant Design Vue CSS-in-JS] ${path ? `Error in ${path}: ` : ''}${message}${
parentSelectors.length ? ` Selector info: ${parentSelectors.join(' -> ')}` : '' parentSelectors.length ? ` Selector: ${parentSelectors.join(' | ')}` : ''
}`, }`,
); );
} }

View File

@ -1,34 +1,36 @@
import type { CSSObject } from '..'; import type { CSSObject } from '..';
import type { Transformer } from './interface'; import type { Transformer } from './interface';
function splitValues(value: string | number) { function splitValues(value: string | number): [values: (string | number)[], important: boolean] {
if (typeof value === 'number') { if (typeof value === 'number') {
return [value]; return [[value], false];
} }
const splitStyle = String(value).split(/\s+/); const rawStyle = String(value).trim();
const importantCells = rawStyle.match(/(.*)(!important)/);
const splitStyle = (importantCells ? importantCells[1] : rawStyle).trim().split(/\s+/);
// Combine styles split in brackets, like `calc(1px + 2px)` // Combine styles split in brackets, like `calc(1px + 2px)`
let temp = ''; let temp = '';
let brackets = 0; let brackets = 0;
return splitStyle.reduce<string[]>((list, item) => { return [
if (item.includes('(')) { splitStyle.reduce<string[]>((list, item) => {
temp += item; if (item.includes('(') || item.includes(')')) {
brackets += item.split('(').length - 1; const left = item.split('(').length - 1;
} else if (item.includes(')')) { const right = item.split(')').length - 1;
temp += ` ${item}`; brackets += left - right;
brackets -= item.split(')').length - 1;
if (brackets === 0) {
list.push(temp);
temp = '';
} }
} else if (brackets > 0) { if (brackets === 0) {
temp += ` ${item}`; list.push(temp + item);
} else { temp = '';
list.push(item); } else if (brackets > 0) {
} temp += item;
return list; }
}, []); return list;
}, []),
!!importantCells,
];
} }
type MatchValue = string[] & { type MatchValue = string[] & {
@ -105,8 +107,14 @@ const keyMap: Record<string, MatchValue> = {
borderEndEndRadius: ['borderBottomRightRadius'], borderEndEndRadius: ['borderBottomRightRadius'],
}; };
function skipCheck(value: string | number) { function wrapImportantAndSkipCheck(value: string | number, important: boolean) {
return { _skip_check_: true, value }; let parsedValue = value;
if (important) {
parsedValue = `${parsedValue} !important`;
}
return { _skip_check_: true, value: parsedValue };
} }
/** /**
@ -127,25 +135,28 @@ const transform: Transformer = {
const matchValue = keyMap[key]; const matchValue = keyMap[key];
if (matchValue && (typeof value === 'number' || typeof value === 'string')) { if (matchValue && (typeof value === 'number' || typeof value === 'string')) {
const values = splitValues(value); const [values, important] = splitValues(value);
if (matchValue.length && matchValue.notSplit) { if (matchValue.length && matchValue.notSplit) {
// not split means always give same value like border // not split means always give same value like border
matchValue.forEach(matchKey => { matchValue.forEach(matchKey => {
clone[matchKey] = skipCheck(value); clone[matchKey] = wrapImportantAndSkipCheck(value, important);
}); });
} else if (matchValue.length === 1) { } else if (matchValue.length === 1) {
// Handle like `marginBlockStart` => `marginTop` // Handle like `marginBlockStart` => `marginTop`
clone[matchValue[0]] = skipCheck(value); clone[matchValue[0]] = wrapImportantAndSkipCheck(value, important);
} else if (matchValue.length === 2) { } else if (matchValue.length === 2) {
// Handle like `marginBlock` => `marginTop` & `marginBottom` // Handle like `marginBlock` => `marginTop` & `marginBottom`
matchValue.forEach((matchKey, index) => { matchValue.forEach((matchKey, index) => {
clone[matchKey] = skipCheck(values[index] ?? values[0]); clone[matchKey] = wrapImportantAndSkipCheck(values[index] ?? values[0], important);
}); });
} else if (matchValue.length === 4) { } else if (matchValue.length === 4) {
// Handle like `inset` => `top` & `right` & `bottom` & `left` // Handle like `inset` => `top` & `right` & `bottom` & `left`
matchValue.forEach((matchKey, index) => { matchValue.forEach((matchKey, index) => {
clone[matchKey] = skipCheck(values[index] ?? values[index - 2] ?? values[0]); clone[matchKey] = wrapImportantAndSkipCheck(
values[index] ?? values[index - 2] ?? values[0],
important,
);
}); });
} else { } else {
clone[key] = value; clone[key] = value;

View File

@ -1,6 +1,7 @@
/** /**
* respect https://github.com/cuth/postcss-pxtorem * respect https://github.com/cuth/postcss-pxtorem
*/ */
// @ts-ignore
import unitless from '@emotion/unitless'; import unitless from '@emotion/unitless';
import type { CSSObject } from '..'; import type { CSSObject } from '..';
import type { Transformer } from './interface'; import type { Transformer } from './interface';

View File

@ -1,5 +1,5 @@
import canUseDom from '../../../../_util/canUseDom'; import canUseDom from '../../canUseDom';
import { ATTR_MARK } from '../../StyleContext'; import { ATTR_MARK } from '../StyleContext';
export const ATTR_CACHE_MAP = 'data-ant-cssinjs-cache-path'; export const ATTR_CACHE_MAP = 'data-ant-cssinjs-cache-path';

View File

@ -0,0 +1,58 @@
export const token2CSSVar = (token: string, prefix = '') => {
return `--${prefix ? `${prefix}-` : ''}${token}`
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, '$1-$2')
.replace(/([a-z])([A-Z0-9])/g, '$1-$2')
.toLowerCase();
};
export const serializeCSSVar = <T extends Record<string, any>>(
cssVars: T,
hashId: string,
options?: {
scope?: string;
},
) => {
if (!Object.keys(cssVars).length) {
return '';
}
return `.${hashId}${options?.scope ? `.${options.scope}` : ''}{${Object.entries(cssVars)
.map(([key, value]) => `${key}:${value};`)
.join('')}}`;
};
export type TokenWithCSSVar<V, T extends Record<string, V> = Record<string, V>> = {
[key in keyof T]?: string | V;
};
export const transformToken = <V, T extends Record<string, V> = Record<string, V>>(
token: T,
themeKey: string,
config?: {
prefix?: string;
ignore?: {
[key in keyof T]?: boolean;
};
unitless?: {
[key in keyof T]?: boolean;
};
preserve?: {
[key in keyof T]?: boolean;
};
scope?: string;
},
): [TokenWithCSSVar<V, T>, string] => {
const cssVars: Record<string, string> = {};
const result: TokenWithCSSVar<V, T> = {};
Object.entries(token).forEach(([key, value]) => {
if (config?.preserve?.[key]) {
result[key as keyof T] = value;
} else if ((typeof value === 'string' || typeof value === 'number') && !config?.ignore?.[key]) {
const cssVar = token2CSSVar(key, config?.prefix);
cssVars[cssVar] =
typeof value === 'number' && !config?.unitless?.[key] ? `${value}px` : String(value);
result[key as keyof T] = `var(${cssVar})`;
}
});
return [result, serializeCSSVar(cssVars, themeKey, { scope: config?.scope })];
};

View File

@ -1,12 +1,37 @@
import hash from '@emotion/hash'; import hash from '@emotion/hash';
import { removeCSS, updateCSS } from '../../vc-util/Dom/dynamicCSS'; import canUseDom from '../../canUseDom';
import canUseDom from '../canUseDom'; import { removeCSS, updateCSS } from '../../../vc-util/Dom/dynamicCSS';
import { ATTR_MARK, ATTR_TOKEN } from '../StyleContext';
import { Theme } from '../theme';
import { Theme } from './theme'; // Create a cache for memo concat
type NestWeakMap<T> = WeakMap<object, NestWeakMap<T> | T>;
const resultCache: NestWeakMap<object> = new WeakMap();
const RESULT_VALUE = {};
export function memoResult<T extends object, R>(callback: () => R, deps: T[]): R {
let current: WeakMap<any, any> = resultCache;
for (let i = 0; i < deps.length; i += 1) {
const dep = deps[i];
if (!current.has(dep)) {
current.set(dep, new WeakMap());
}
current = current.get(dep)!;
}
if (!current.has(RESULT_VALUE)) {
current.set(RESULT_VALUE, callback());
}
return current.get(RESULT_VALUE);
}
// Create a cache here to avoid always loop generate // Create a cache here to avoid always loop generate
const flattenTokenCache = new WeakMap<any, string>(); const flattenTokenCache = new WeakMap<any, string>();
/**
* Flatten token to string, this will auto cache the result when token not change
*/
export function flattenToken(token: any) { export function flattenToken(token: any) {
let str = flattenTokenCache.get(token) || ''; let str = flattenTokenCache.get(token) || '';
@ -116,3 +141,39 @@ export function supportLogicProps(): boolean {
return canLogic!; return canLogic!;
} }
export const isClientSide = canUseDom();
export function unit(num: string | number) {
if (typeof num === 'number') {
return `${num}px`;
}
return num;
}
export function toStyleStr(
style: string,
tokenKey?: string,
styleId?: string,
customizeAttrs: Record<string, string> = {},
plain = false,
) {
if (plain) {
return style;
}
const attrs: Record<string, string | undefined> = {
...customizeAttrs,
[ATTR_TOKEN]: tokenKey,
[ATTR_MARK]: styleId,
};
const attrStr = Object.keys(attrs)
.map(attr => {
const val = attrs[attr];
return val ? `${attr}="${val}"` : null;
})
.filter(v => v)
.join(' ');
return `<style ${attrStr}>${style}</style>`;
}

View File

@ -2,32 +2,31 @@ export function isWindow(obj: any): obj is Window {
return obj !== null && obj !== undefined && obj === obj.window; return obj !== null && obj !== undefined && obj === obj.window;
} }
export default function getScroll( const getScroll = (target: HTMLElement | Window | Document | null): number => {
target: HTMLElement | Window | Document | null,
top: boolean,
): number {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return 0; return 0;
} }
const method = top ? 'scrollTop' : 'scrollLeft';
let result = 0; let result = 0;
if (isWindow(target)) { if (isWindow(target)) {
result = target[top ? 'scrollY' : 'scrollX']; result = target.pageYOffset;
} else if (target instanceof Document) { } else if (target instanceof Document) {
result = target.documentElement[method]; result = target.documentElement.scrollTop;
} else if (target instanceof HTMLElement) { } else if (target instanceof HTMLElement) {
result = target[method]; result = target.scrollTop;
} else if (target) { } else if (target) {
// According to the type inference, the `target` is `never` type. // According to the type inference, the `target` is `never` type.
// Since we configured the loose mode type checking, and supports mocking the target with such shape below:: // Since we configured the loose mode type checking, and supports mocking the target with such shape below::
// `{ documentElement: { scrollLeft: 200, scrollTop: 400 } }`, // `{ documentElement: { scrollLeft: 200, scrollTop: 400 } }`,
// the program may falls into this branch. // the program may falls into this branch.
// Check the corresponding tests for details. Don't sure what is the real scenario this happens. // Check the corresponding tests for details. Don't sure what is the real scenario this happens.
result = target[method]; /* biome-ignore lint/complexity/useLiteralKeys: target is a never type */ /* eslint-disable-next-line dot-notation */
result = target['scrollTop'];
} }
if (target && !isWindow(target) && typeof result !== 'number') { if (target && !isWindow(target) && typeof result !== 'number') {
result = ((target.ownerDocument ?? target) as any).documentElement?.[method]; result = (target.ownerDocument ?? (target as Document)).documentElement?.scrollTop;
} }
return result; return result;
} };
export default getScroll;

View File

@ -0,0 +1,48 @@
import type { Ref, ShallowRef } from 'vue';
import { shallowRef, ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
function useLayoutEffect(
fn: (mount: boolean) => void | VoidFunction,
deps?: Ref<any> | Ref<any>[] | ShallowRef<any> | ShallowRef<any>[],
) {
const firstMount = shallowRef(true);
const cleanupFn = ref(null);
let stopWatch = null;
stopWatch = watch(
deps,
() => {
nextTick(() => {
if (cleanupFn.value) {
cleanupFn.value();
}
cleanupFn.value = fn(firstMount.value);
});
},
{ immediate: true, flush: 'post' },
);
onMounted(() => {
firstMount.value = false;
});
onUnmounted(() => {
if (cleanupFn.value) {
cleanupFn.value();
}
if (stopWatch) {
stopWatch();
}
});
}
export const useLayoutUpdateEffect = (callback, deps) => {
useLayoutEffect(firstMount => {
if (!firstMount) {
return callback();
}
}, deps);
};
export default useLayoutEffect;

View File

@ -14,7 +14,7 @@ interface ScrollToOptions {
export default function scrollTo(y: number, options: ScrollToOptions = {}) { export default function scrollTo(y: number, options: ScrollToOptions = {}) {
const { getContainer = () => window, callback, duration = 450 } = options; const { getContainer = () => window, callback, duration = 450 } = options;
const container = getContainer(); const container = getContainer();
const scrollTop = getScroll(container, true); const scrollTop = getScroll(container);
const startTime = Date.now(); const startTime = Date.now();
const frameFunc = () => { const frameFunc = () => {

View File

@ -27,10 +27,11 @@ import useStyle from './style';
function getDefaultTarget() { function getDefaultTarget() {
return typeof window !== 'undefined' ? window : null; return typeof window !== 'undefined' ? window : null;
} }
enum AffixStatus { const AFFIX_STATUS_NONE = 0;
None, const AFFIX_STATUS_PREPARE = 1;
Prepare,
} type AffixStatus = typeof AFFIX_STATUS_NONE | typeof AFFIX_STATUS_PREPARE;
export interface AffixState { export interface AffixState {
affixStyle?: CSSProperties; affixStyle?: CSSProperties;
placeholderStyle?: CSSProperties; placeholderStyle?: CSSProperties;
@ -82,7 +83,7 @@ const Affix = defineComponent({
const state = reactive({ const state = reactive({
affixStyle: undefined, affixStyle: undefined,
placeholderStyle: undefined, placeholderStyle: undefined,
status: AffixStatus.None, status: AFFIX_STATUS_NONE,
lastAffix: false, lastAffix: false,
prevTarget: null, prevTarget: null,
timeout: null, timeout: null,
@ -98,7 +99,12 @@ const Affix = defineComponent({
const measure = () => { const measure = () => {
const { status, lastAffix } = state; const { status, lastAffix } = state;
const { target } = props; const { target } = props;
if (status !== AffixStatus.Prepare || !fixedNode.value || !placeholderNode.value || !target) { if (
status !== AFFIX_STATUS_PREPARE ||
!fixedNode.value ||
!placeholderNode.value ||
!target
) {
return; return;
} }
@ -108,7 +114,7 @@ const Affix = defineComponent({
} }
const newState = { const newState = {
status: AffixStatus.None, status: AFFIX_STATUS_NONE,
} as AffixState; } as AffixState;
const placeholderRect = getTargetRect(placeholderNode.value as HTMLElement); const placeholderRect = getTargetRect(placeholderNode.value as HTMLElement);
@ -172,7 +178,7 @@ const Affix = defineComponent({
}; };
const prepareMeasure = () => { const prepareMeasure = () => {
Object.assign(state, { Object.assign(state, {
status: AffixStatus.Prepare, status: AFFIX_STATUS_PREPARE,
affixStyle: undefined, affixStyle: undefined,
placeholderStyle: undefined, placeholderStyle: undefined,
}); });
@ -253,12 +259,13 @@ const Affix = defineComponent({
}); });
const { prefixCls } = useConfigInject('affix', props); const { prefixCls } = useConfigInject('affix', props);
const [wrapSSR, hashId] = useStyle(prefixCls); const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls);
return () => { return () => {
const { affixStyle, placeholderStyle, status } = state; const { affixStyle, placeholderStyle, status } = state;
const className = classNames({ const className = classNames({
[prefixCls.value]: affixStyle, [prefixCls.value]: affixStyle,
[hashId.value]: true, [hashId.value]: true,
[cssVarCls.value]: true,
}); });
const restProps = omit(props, [ const restProps = omit(props, [
'prefixCls', 'prefixCls',

View File

@ -1,15 +1,21 @@
import type { CSSObject } from '../../_util/cssinjs'; import type { CSSObject } from '../../_util/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal'; import { FullToken, GenerateStyle, genStyleHooks, GetDefaultToken } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
export interface ComponentToken {
/**
* @desc z-index
* @descEN z-index of popup
*/
zIndexPopup: number;
}
interface AffixToken extends FullToken<'Affix'> { interface AffixToken extends FullToken<'Affix'> {
zIndexPopup: number; //
} }
// ============================== Shared ============================== // ============================== Shared ==============================
const genSharedAffixStyle: GenerateStyle<AffixToken> = (token): CSSObject => { const genSharedAffixStyle: GenerateStyle<AffixToken> = (token): CSSObject => {
const { componentCls } = token; const { componentCls } = token;
return { return {
[componentCls]: { [componentCls]: {
position: 'fixed', position: 'fixed',
@ -18,10 +24,9 @@ const genSharedAffixStyle: GenerateStyle<AffixToken> = (token): CSSObject => {
}; };
}; };
// ============================== Export ============================== export const prepareComponentToken: GetDefaultToken<'Affix'> = token => ({
export default genComponentStyleHook('Affix', token => { zIndexPopup: token.zIndexBase + 10,
const affixToken = mergeToken<AffixToken>(token, {
zIndexPopup: token.zIndexBase + 10,
});
return [genSharedAffixStyle(affixToken)];
}); });
// ============================== Export ==============================
export default genStyleHooks('Affix', genSharedAffixStyle, prepareComponentToken);

View File

@ -9,8 +9,11 @@ export function getTargetRect(target: BindElement): DOMRect {
: ({ top: 0, bottom: window.innerHeight } as DOMRect); : ({ top: 0, bottom: window.innerHeight } as DOMRect);
} }
export function getFixedTop(placeholderRect: DOMRect, targetRect: DOMRect, offsetTop: number) { export function getFixedTop(placeholderRect: DOMRect, targetRect: DOMRect, offsetTop?: number) {
if (offsetTop !== undefined && targetRect.top > placeholderRect.top - offsetTop) { if (
offsetTop !== undefined &&
Math.round(targetRect.top) > Math.round(placeholderRect.top) - offsetTop
) {
return `${offsetTop + targetRect.top}px`; return `${offsetTop + targetRect.top}px`;
} }
return undefined; return undefined;
@ -19,9 +22,12 @@ export function getFixedTop(placeholderRect: DOMRect, targetRect: DOMRect, offse
export function getFixedBottom( export function getFixedBottom(
placeholderRect: DOMRect, placeholderRect: DOMRect,
targetRect: DOMRect, targetRect: DOMRect,
offsetBottom: number, offsetBottom?: number,
) { ) {
if (offsetBottom !== undefined && targetRect.bottom < placeholderRect.bottom + offsetBottom) { if (
offsetBottom !== undefined &&
Math.round(targetRect.bottom) < Math.round(placeholderRect.bottom) + offsetBottom
) {
const targetBottomOffset = window.innerHeight - targetRect.bottom; const targetBottomOffset = window.innerHeight - targetRect.bottom;
return `${offsetBottom + targetBottomOffset}px`; return `${offsetBottom + targetBottomOffset}px`;
} }
@ -29,7 +35,7 @@ export function getFixedBottom(
} }
// ======================== Observer ======================== // ======================== Observer ========================
const TRIGGER_EVENTS = [ const TRIGGER_EVENTS: (keyof WindowEventMap)[] = [
'resize', 'resize',
'scroll', 'scroll',
'touchstart', 'touchstart',

View File

@ -70,7 +70,7 @@ const Alert = defineComponent({
props: alertProps(), props: alertProps(),
setup(props, { slots, emit, attrs, expose }) { setup(props, { slots, emit, attrs, expose }) {
const { prefixCls, direction } = useConfigInject('alert', props); const { prefixCls, direction } = useConfigInject('alert', props);
const [wrapSSR, hashId] = useStyle(prefixCls); const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls);
const closing = shallowRef(false); const closing = shallowRef(false);
const closed = shallowRef(false); const closed = shallowRef(false);
const alertNode = shallowRef(); const alertNode = shallowRef();
@ -134,6 +134,7 @@ const Alert = defineComponent({
[`${prefixClsValue}-closable`]: closable, [`${prefixClsValue}-closable`]: closable,
[`${prefixClsValue}-rtl`]: direction.value === 'rtl', [`${prefixClsValue}-rtl`]: direction.value === 'rtl',
[hashId.value]: true, [hashId.value]: true,
[cssVarCls.value]: true,
}); });
const closeIcon = closable ? ( const closeIcon = closable ? (

View File

@ -1,13 +1,29 @@
import type { CSSInterpolation, CSSObject } from '../../_util/cssinjs'; import { CSSObject, unit } from '../../_util/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal'; import { FullToken, GenerateStyle, genStyleHooks, GetDefaultToken } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { resetComponent } from '../../style'; import { resetComponent } from '../../style';
import { CSSProperties } from 'vue';
export interface ComponentToken {} export interface ComponentToken {
// Component token here
/**
* @desc
* @descEN Default padding
*/
defaultPadding: CSSProperties['padding'];
/**
* @desc
* @descEN Padding with description
*/
withDescriptionPadding: CSSProperties['padding'];
/**
* @desc
* @descEN Icon size with description
*/
withDescriptionIconSize: number;
}
type AlertToken = FullToken<'Alert'> & { type AlertToken = FullToken<'Alert'> & {
alertIconSizeLG: number; // Custom token here
alertPaddingHorizontal: number;
}; };
const genAlertTypeStyle = ( const genAlertTypeStyle = (
@ -17,8 +33,8 @@ const genAlertTypeStyle = (
token: AlertToken, token: AlertToken,
alertCls: string, alertCls: string,
): CSSObject => ({ ): CSSObject => ({
backgroundColor: bgColor, background: bgColor,
border: `${token.lineWidth}px ${token.lineType} ${borderColor}`, border: `${unit(token.lineWidth)} ${token.lineType} ${borderColor}`,
[`${alertCls}-icon`]: { [`${alertCls}-icon`]: {
color: iconColor, color: iconColor,
}, },
@ -35,12 +51,11 @@ export const genBaseStyle: GenerateStyle<AlertToken> = (token: AlertToken): CSSO
lineHeight, lineHeight,
borderRadiusLG: borderRadius, borderRadiusLG: borderRadius,
motionEaseInOutCirc, motionEaseInOutCirc,
alertIconSizeLG, withDescriptionIconSize,
colorText, colorText,
paddingContentVerticalSM, colorTextHeading,
alertPaddingHorizontal, withDescriptionPadding,
paddingMD, defaultPadding,
paddingContentHorizontalLG,
} = token; } = token;
return { return {
@ -49,7 +64,7 @@ export const genBaseStyle: GenerateStyle<AlertToken> = (token: AlertToken): CSSO
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
padding: `${paddingContentVerticalSM}px ${alertPaddingHorizontal}px`, // Fixed horizontal padding here. padding: defaultPadding,
wordWrap: 'break-word', wordWrap: 'break-word',
borderRadius, borderRadius,
@ -67,14 +82,14 @@ export const genBaseStyle: GenerateStyle<AlertToken> = (token: AlertToken): CSSO
lineHeight: 0, lineHeight: 0,
}, },
[`&-description`]: { '&-description': {
display: 'none', display: 'none',
fontSize, fontSize,
lineHeight, lineHeight,
}, },
'&-message': { '&-message': {
color: colorText, color: colorTextHeading,
}, },
[`&${componentCls}-motion-leave`]: { [`&${componentCls}-motion-leave`]: {
@ -96,24 +111,23 @@ export const genBaseStyle: GenerateStyle<AlertToken> = (token: AlertToken): CSSO
[`${componentCls}-with-description`]: { [`${componentCls}-with-description`]: {
alignItems: 'flex-start', alignItems: 'flex-start',
paddingInline: paddingContentHorizontalLG, padding: withDescriptionPadding,
paddingBlock: paddingMD,
[`${componentCls}-icon`]: { [`${componentCls}-icon`]: {
marginInlineEnd: marginSM, marginInlineEnd: marginSM,
fontSize: alertIconSizeLG, fontSize: withDescriptionIconSize,
lineHeight: 0, lineHeight: 0,
}, },
[`${componentCls}-message`]: { [`${componentCls}-message`]: {
display: 'block', display: 'block',
marginBottom: marginXS, marginBottom: marginXS,
color: colorText, color: colorTextHeading,
fontSize: fontSizeLG, fontSize: fontSizeLG,
}, },
[`${componentCls}-description`]: { [`${componentCls}-description`]: {
display: 'block', display: 'block',
color: colorText,
}, },
}, },
@ -187,7 +201,7 @@ export const genActionStyle: GenerateStyle<AlertToken> = (token: AlertToken): CS
return { return {
[componentCls]: { [componentCls]: {
[`&-action`]: { '&-action': {
marginInlineStart: marginXS, marginInlineStart: marginXS,
}, },
@ -196,7 +210,7 @@ export const genActionStyle: GenerateStyle<AlertToken> = (token: AlertToken): CS
padding: 0, padding: 0,
overflow: 'hidden', overflow: 'hidden',
fontSize: fontSizeIcon, fontSize: fontSizeIcon,
lineHeight: `${fontSizeIcon}px`, lineHeight: unit(fontSizeIcon),
backgroundColor: 'transparent', backgroundColor: 'transparent',
border: 'none', border: 'none',
outline: 'none', outline: 'none',
@ -222,19 +236,17 @@ export const genActionStyle: GenerateStyle<AlertToken> = (token: AlertToken): CS
}; };
}; };
export const genAlertStyle: GenerateStyle<AlertToken> = (token: AlertToken): CSSInterpolation => [ export const prepareComponentToken: GetDefaultToken<'Alert'> = token => {
genBaseStyle(token), const paddingHorizontal = 12; // Fixed value here.
genTypeStyle(token), return {
genActionStyle(token), withDescriptionIconSize: token.fontSizeHeading3,
]; defaultPadding: `${token.paddingContentVerticalSM}px ${paddingHorizontal}px`,
withDescriptionPadding: `${token.paddingMD}px ${token.paddingContentHorizontalLG}px`,
};
};
export default genComponentStyleHook('Alert', token => { export default genStyleHooks(
const { fontSizeHeading3 } = token; 'Alert',
token => [genBaseStyle(token), genTypeStyle(token), genActionStyle(token)],
const alertToken = mergeToken<AlertToken>(token, { prepareComponentToken,
alertIconSizeLG: fontSizeHeading3, );
alertPaddingHorizontal: 12, // Fixed value here.
});
return [genAlertStyle(alertToken)];
});

View File

@ -23,6 +23,7 @@ import AnchorLink from './AnchorLink';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import devWarning from '../vc-util/devWarning'; import devWarning from '../vc-util/devWarning';
import { arrayType } from '../_util/type'; import { arrayType } from '../_util/type';
import useCSSVarCls from '../config-provider/hooks/useCssVarCls';
export type AnchorDirection = 'vertical' | 'horizontal'; export type AnchorDirection = 'vertical' | 'horizontal';
@ -39,8 +40,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
if (rect.width || rect.height) { if (rect.width || rect.height) {
if (container === window) { if (container === window) {
container = element.ownerDocument!.documentElement!; return rect.top - element.ownerDocument!.documentElement!.clientTop;
return rect.top - container.clientTop;
} }
return rect.top - (container as HTMLElement).getBoundingClientRect().top; return rect.top - (container as HTMLElement).getBoundingClientRect().top;
} }
@ -70,6 +70,7 @@ export const anchorProps = () => ({
targetOffset: Number, targetOffset: Number,
items: arrayType<AnchorLinkItemProps[]>(), items: arrayType<AnchorLinkItemProps[]>(),
direction: PropTypes.oneOf(['vertical', 'horizontal'] as AnchorDirection[]).def('vertical'), direction: PropTypes.oneOf(['vertical', 'horizontal'] as AnchorDirection[]).def('vertical'),
replace: Boolean,
onChange: Function as PropType<(currentActiveLink: string) => void>, onChange: Function as PropType<(currentActiveLink: string) => void>,
onClick: Function as PropType<(e: MouseEvent, link: { title: any; href: string }) => void>, onClick: Function as PropType<(e: MouseEvent, link: { title: any; href: string }) => void>,
}); });
@ -91,7 +92,7 @@ export default defineComponent({
setup(props, { emit, attrs, slots, expose }) { setup(props, { emit, attrs, slots, expose }) {
const { prefixCls, getTargetContainer, direction } = useConfigInject('anchor', props); const { prefixCls, getTargetContainer, direction } = useConfigInject('anchor', props);
const anchorDirection = computed(() => props.direction ?? 'vertical'); const anchorDirection = computed(() => props.direction ?? 'vertical');
const rootCls = useCSSVarCls(prefixCls);
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
devWarning( devWarning(
props.items && typeof slots.default !== 'function', props.items && typeof slots.default !== 'function',
@ -133,7 +134,7 @@ export default defineComponent({
const target = document.getElementById(sharpLinkMatch[1]); const target = document.getElementById(sharpLinkMatch[1]);
if (target) { if (target) {
const top = getOffsetTop(target, container); const top = getOffsetTop(target, container);
if (top < offsetTop + bounds) { if (top <= offsetTop + bounds) {
linkSections.push({ linkSections.push({
link, link,
top, top,
@ -170,7 +171,7 @@ export default defineComponent({
} }
const container = getContainer.value(); const container = getContainer.value();
const scrollTop = getScroll(container, true); const scrollTop = getScroll(container);
const eleOffsetTop = getOffsetTop(targetElement, container); const eleOffsetTop = getOffsetTop(targetElement, container);
let y = scrollTop + eleOffsetTop; let y = scrollTop + eleOffsetTop;
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0; y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
@ -277,6 +278,7 @@ export default defineComponent({
title={title} title={title}
customTitleProps={option} customTitleProps={option}
v-slots={{ customTitle: slots.customTitle }} v-slots={{ customTitle: slots.customTitle }}
replace={props.replace}
> >
{anchorDirection.value === 'vertical' ? createNestedLink(children) : null} {anchorDirection.value === 'vertical' ? createNestedLink(children) : null}
</AnchorLink> </AnchorLink>
@ -284,7 +286,7 @@ export default defineComponent({
}) })
: null; : null;
const [wrapSSR, hashId] = useStyle(prefixCls); const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
return () => { return () => {
const { offsetTop, affix, showInkInFixed } = props; const { offsetTop, affix, showInkInFixed } = props;
@ -296,6 +298,8 @@ export default defineComponent({
const wrapperClass = classNames(hashId.value, props.wrapperClass, `${pre}-wrapper`, { const wrapperClass = classNames(hashId.value, props.wrapperClass, `${pre}-wrapper`, {
[`${pre}-wrapper-horizontal`]: anchorDirection.value === 'horizontal', [`${pre}-wrapper-horizontal`]: anchorDirection.value === 'horizontal',
[`${pre}-rtl`]: direction.value === 'rtl', [`${pre}-rtl`]: direction.value === 'rtl',
[rootCls.value]: true,
[cssVarCls.value]: true,
}); });
const anchorClass = classNames(pre, { const anchorClass = classNames(pre, {

View File

@ -13,6 +13,7 @@ export const anchorLinkProps = () => ({
href: String, href: String,
title: anyType<VueNode | ((item: any) => VueNode)>(), title: anyType<VueNode | ((item: any) => VueNode)>(),
target: String, target: String,
replace: Boolean,
/* private use */ /* private use */
customTitleProps: objectType<AnchorLinkItemProps>(), customTitleProps: objectType<AnchorLinkItemProps>(),
}); });
@ -53,6 +54,10 @@ export default defineComponent({
const { href } = props; const { href } = props;
contextHandleClick(e, { title: mergedTitle, href }); contextHandleClick(e, { title: mergedTitle, href });
scrollTo(href); scrollTo(href);
if (props.replace) {
e.preventDefault();
window.location.replace(href);
}
}; };
watch( watch(

View File

@ -1,21 +1,55 @@
import type { CSSObject } from '../../_util/cssinjs'; import { unit } from '../../_util/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal'; import {
import { genComponentStyleHook, mergeToken } from '../../theme/internal'; FullToken,
GenerateStyle,
genStyleHooks,
GetDefaultToken,
mergeToken,
} from '../../theme/internal';
import { resetComponent, textEllipsis } from '../../style'; import { resetComponent, textEllipsis } from '../../style';
export interface ComponentToken {} export interface ComponentToken {
/**
* @desc
* @descEN Vertical padding of link
*/
linkPaddingBlock: number;
/**
* @desc
* @descEN Horizontal padding of link
*/
linkPaddingInlineStart: number;
}
/**
* @desc Anchor Token
* @descEN Token for Anchor component
*/
interface AnchorToken extends FullToken<'Anchor'> { interface AnchorToken extends FullToken<'Anchor'> {
/**
* @desc
* @descEN Holder block offset
*/
holderOffsetBlock: number; holderOffsetBlock: number;
anchorPaddingBlock: number; /**
anchorPaddingBlockSecondary: number; * @desc
anchorPaddingInline: number; * @descEN Secondary anchor block padding
anchorBallSize: number; */
anchorTitleBlock: number; anchorPaddingBlockSecondary: number | string;
/**
* @desc
* @descEN Anchor ball size
*/
anchorBallSize: number | string;
/**
* @desc
* @descEN Anchor title block
*/
anchorTitleBlock: number | string;
} }
// ============================== Shared ============================== // ============================== Shared ==============================
const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => { const genSharedAnchorStyle: GenerateStyle<AnchorToken> = token => {
const { const {
componentCls, componentCls,
holderOffsetBlock, holderOffsetBlock,
@ -24,26 +58,25 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
colorPrimary, colorPrimary,
lineType, lineType,
colorSplit, colorSplit,
calc,
} = token; } = token;
return { return {
[`${componentCls}-wrapper`]: { [`${componentCls}-wrapper`]: {
marginBlockStart: -holderOffsetBlock, marginBlockStart: calc(holderOffsetBlock).mul(-1).equal(),
paddingBlockStart: holderOffsetBlock, paddingBlockStart: holderOffsetBlock,
// delete overflow: auto // delete overflow: auto
// overflow: 'auto', // overflow: 'auto',
backgroundColor: 'transparent',
[componentCls]: { [componentCls]: {
...resetComponent(token), ...resetComponent(token),
position: 'relative', position: 'relative',
paddingInlineStart: lineWidthBold, paddingInlineStart: lineWidthBold,
[`${componentCls}-link`]: { [`${componentCls}-link`]: {
paddingBlock: token.anchorPaddingBlock, paddingBlock: token.linkPaddingBlock,
paddingInline: `${token.anchorPaddingInline}px 0`, paddingInline: `${unit(token.linkPaddingInlineStart)} 0`,
'&-title': { '&-title': {
...textEllipsis, ...textEllipsis,
@ -73,28 +106,21 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
[componentCls]: { [componentCls]: {
'&::before': { '&::before': {
position: 'absolute', position: 'absolute',
left: { insetInlineStart: 0,
_skip_check_: true,
value: 0,
},
top: 0, top: 0,
height: '100%', height: '100%',
borderInlineStart: `${lineWidthBold}px ${lineType} ${colorSplit}`, borderInlineStart: `${unit(lineWidthBold)} ${lineType} ${colorSplit}`,
content: '" "', content: '" "',
}, },
[`${componentCls}-ink`]: { [`${componentCls}-ink`]: {
position: 'absolute', position: 'absolute',
left: { insetInlineStart: 0,
_skip_check_: true,
value: 0,
},
display: 'none', display: 'none',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
transition: `top ${motionDurationSlow} ease-in-out`, transition: `top ${motionDurationSlow} ease-in-out`,
width: lineWidthBold, width: lineWidthBold,
backgroundColor: colorPrimary, backgroundColor: colorPrimary,
[`&${componentCls}-ink-visible`]: { [`&${componentCls}-ink-visible`]: {
display: 'inline-block', display: 'inline-block',
}, },
@ -109,7 +135,7 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
}; };
}; };
const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = (token): CSSObject => { const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = token => {
const { componentCls, motionDurationSlow, lineWidthBold, colorPrimary } = token; const { componentCls, motionDurationSlow, lineWidthBold, colorPrimary } = token;
return { return {
@ -127,7 +153,7 @@ const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = (token): CSSO
value: 0, value: 0,
}, },
bottom: 0, bottom: 0,
borderBottom: `1px ${token.lineType} ${token.colorSplit}`, borderBottom: `${unit(token.lineWidth)} ${token.lineType} ${token.colorSplit}`,
content: '" "', content: '" "',
}, },
@ -157,17 +183,23 @@ const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = (token): CSSO
}; };
}; };
// ============================== Export ============================== export const prepareComponentToken: GetDefaultToken<'Anchor'> = token => ({
export default genComponentStyleHook('Anchor', token => { linkPaddingBlock: token.paddingXXS,
const { fontSize, fontSizeLG, padding, paddingXXS } = token; linkPaddingInlineStart: token.padding,
const anchorToken = mergeToken<AnchorToken>(token, {
holderOffsetBlock: paddingXXS,
anchorPaddingBlock: paddingXXS,
anchorPaddingBlockSecondary: paddingXXS / 2,
anchorPaddingInline: padding,
anchorTitleBlock: (fontSize / 14) * 3,
anchorBallSize: fontSizeLG / 2,
});
return [genSharedAnchorStyle(anchorToken), genSharedAnchorHorizontalStyle(anchorToken)];
}); });
// ============================== Export ==============================
export default genStyleHooks(
'Anchor',
token => {
const { fontSize, fontSizeLG, paddingXXS, calc } = token;
const anchorToken = mergeToken<AnchorToken>(token, {
holderOffsetBlock: paddingXXS,
anchorPaddingBlockSecondary: calc(paddingXXS).div(2).equal(),
anchorTitleBlock: calc(fontSize).div(14).mul(3).equal(),
anchorBallSize: calc(fontSizeLG).div(2).equal(),
});
return [genSharedAnchorStyle(anchorToken), genSharedAnchorHorizontalStyle(anchorToken)];
},
prepareComponentToken,
);

View File

@ -4,6 +4,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { genPresetColor, resetComponent } from '../../style'; import { genPresetColor, resetComponent } from '../../style';
export interface ComponentToken {}
interface BadgeToken extends FullToken<'Badge'> { interface BadgeToken extends FullToken<'Badge'> {
badgeFontHeight: number; badgeFontHeight: number;
badgeZIndex: number | string; badgeZIndex: number | string;

View File

@ -3,6 +3,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { genFocusStyle, resetComponent } from '../../style'; import { genFocusStyle, resetComponent } from '../../style';
export interface ComponentToken {}
interface BreadcrumbToken extends FullToken<'Breadcrumb'> { interface BreadcrumbToken extends FullToken<'Breadcrumb'> {
breadcrumbBaseColor: string; breadcrumbBaseColor: string;
breadcrumbFontSize: number; breadcrumbFontSize: number;

View File

@ -45,7 +45,11 @@ export default defineComponent({
break; break;
default: default:
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
devWarning(!size, 'Button.Group', 'Invalid prop `size`.'); devWarning(
!size || ['large', 'small', 'middle'].includes(size),
'Button.Group',
'Invalid prop `size`.',
);
} }
return { return {
[`${prefixCls.value}`]: true, [`${prefixCls.value}`]: true,

View File

@ -45,7 +45,7 @@ export default defineComponent({
// emits: ['click', 'mousedown'], // emits: ['click', 'mousedown'],
setup(props, { slots, attrs, emit, expose }) { setup(props, { slots, attrs, emit, expose }) {
const { prefixCls, autoInsertSpaceInButton, direction, size } = useConfigInject('btn', props); const { prefixCls, autoInsertSpaceInButton, direction, size } = useConfigInject('btn', props);
const [wrapSSR, hashId] = useStyle(prefixCls); const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const groupSizeContext = GroupSizeContext.useInject(); const groupSizeContext = GroupSizeContext.useInject();
const disabledContext = useInjectDisabled(); const disabledContext = useInjectDisabled();
const mergedDisabled = computed(() => props.disabled ?? disabledContext.value); const mergedDisabled = computed(() => props.disabled ?? disabledContext.value);
@ -95,6 +95,7 @@ export default defineComponent({
compactItemClassnames.value, compactItemClassnames.value,
{ {
[hashId.value]: true, [hashId.value]: true,
[cssVarCls.value]: true,
[`${pre}`]: true, [`${pre}`]: true,
[`${pre}-${shape}`]: shape !== 'default' && shape, [`${pre}-${shape}`]: shape !== 'default' && shape,
[`${pre}-${type}`]: type, [`${pre}-${type}`]: type,
@ -216,7 +217,7 @@ export default defineComponent({
); );
if (href !== undefined) { if (href !== undefined) {
return wrapSSR( return wrapCSSVar(
<a {...buttonProps} href={href} target={target} ref={buttonNodeRef}> <a {...buttonProps} href={href} target={target} ref={buttonNodeRef}>
{iconNode} {iconNode}
{kids} {kids}
@ -239,7 +240,7 @@ export default defineComponent({
); );
} }
return wrapSSR(buttonNode); return wrapCSSVar(buttonNode);
}; };
}, },
}); });

View File

@ -0,0 +1,72 @@
// Style as inline component
import type { ButtonToken } from './token';
import { prepareComponentToken, prepareToken } from './token';
import { genCompactItemStyle } from '../../style/compact-item';
import { genCompactItemVerticalStyle } from '../../style/compact-item-vertical';
import type { GenerateStyle } from '../../theme/internal';
import { genSubStyleComponent } from '../../theme/internal';
import type { CSSObject } from '../../_util/cssinjs';
import { unit } from '../../_util/cssinjs';
const genButtonCompactStyle: GenerateStyle<ButtonToken, CSSObject> = token => {
const { componentCls, calc } = token;
return {
[componentCls]: {
// Special styles for Primary Button
[`&-compact-item${componentCls}-primary`]: {
[`&:not([disabled]) + ${componentCls}-compact-item${componentCls}-primary:not([disabled])`]:
{
position: 'relative',
'&:before': {
position: 'absolute',
top: calc(token.lineWidth).mul(-1).equal(),
insetInlineStart: calc(token.lineWidth).mul(-1).equal(),
display: 'inline-block',
width: token.lineWidth,
height: `calc(100% + ${unit(token.lineWidth)} * 2)`,
backgroundColor: token.colorPrimaryHover,
content: '""',
},
},
},
// Special styles for Primary Button
'&-compact-vertical-item': {
[`&${componentCls}-primary`]: {
[`&:not([disabled]) + ${componentCls}-compact-vertical-item${componentCls}-primary:not([disabled])`]:
{
position: 'relative',
'&:before': {
position: 'absolute',
top: calc(token.lineWidth).mul(-1).equal(),
insetInlineStart: calc(token.lineWidth).mul(-1).equal(),
display: 'inline-block',
width: `calc(100% + ${unit(token.lineWidth)} * 2)`,
height: token.lineWidth,
backgroundColor: token.colorPrimaryHover,
content: '""',
},
},
},
},
},
};
};
// ============================== Export ==============================
export default genSubStyleComponent(
['Button', 'compact'],
token => {
const buttonToken = prepareToken(token);
return [
// Space Compact
genCompactItemStyle(buttonToken),
genCompactItemVerticalStyle(buttonToken),
genButtonCompactStyle(buttonToken),
] as CSSObject[];
},
prepareComponentToken,
);

View File

@ -1,4 +1,5 @@
import type { ButtonToken } from '.'; import type { CSSObject } from '../../_util/cssinjs';
import type { ButtonToken } from './token';
import type { GenerateStyle } from '../../theme/internal'; import type { GenerateStyle } from '../../theme/internal';
const genButtonBorderStyle = (buttonTypeCls: string, borderColor: string) => ({ const genButtonBorderStyle = (buttonTypeCls: string, borderColor: string) => ({
@ -22,8 +23,8 @@ const genButtonBorderStyle = (buttonTypeCls: string, borderColor: string) => ({
}, },
}); });
const genGroupStyle: GenerateStyle<ButtonToken> = token => { const genGroupStyle: GenerateStyle<ButtonToken, CSSObject> = token => {
const { componentCls, fontSize, lineWidth, colorPrimaryHover, colorErrorHover } = token; const { componentCls, fontSize, lineWidth, groupBorderColor, colorErrorHover } = token;
return { return {
[`${componentCls}-group`]: [ [`${componentCls}-group`]: [
@ -41,7 +42,7 @@ const genGroupStyle: GenerateStyle<ButtonToken> = token => {
}, },
'&:not(:first-child)': { '&:not(:first-child)': {
marginInlineStart: -lineWidth, marginInlineStart: token.calc(lineWidth).mul(-1).equal(),
[`&, & > ${componentCls}`]: { [`&, & > ${componentCls}`]: {
borderStartStartRadius: 0, borderStartStartRadius: 0,
@ -71,7 +72,7 @@ const genGroupStyle: GenerateStyle<ButtonToken> = token => {
}, },
// Border Color // Border Color
genButtonBorderStyle(`${componentCls}-primary`, colorPrimaryHover), genButtonBorderStyle(`${componentCls}-primary`, groupBorderColor),
genButtonBorderStyle(`${componentCls}-danger`, colorErrorHover), genButtonBorderStyle(`${componentCls}-danger`, colorErrorHover),
], ],
}; };

View File

@ -1,51 +1,59 @@
import type { CSSInterpolation, CSSObject } from '../../_util/cssinjs'; import type { CSSObject } from '../../_util/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal'; import { unit } from '../../_util/cssinjs';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import genGroupStyle from './group';
import { genFocusStyle } from '../../style'; import { genFocusStyle } from '../../style';
import { genCompactItemStyle } from '../../style/compact-item'; import type { GenerateStyle } from '../../theme/internal';
import { genCompactItemVerticalStyle } from '../../style/compact-item-vertical'; import { genStyleHooks, mergeToken } from '../../theme/internal';
import genGroupStyle from './group';
import type { ButtonToken, ComponentToken } from './token';
import { prepareComponentToken, prepareToken } from './token';
/** Component only token. Which will handle additional calculation of alias token */ export type { ComponentToken };
export interface ComponentToken {}
export interface ButtonToken extends FullToken<'Button'> {
// FIXME: should be removed
colorOutlineDefault: string;
buttonPaddingHorizontal: number;
}
// ============================== Shared ============================== // ============================== Shared ==============================
const genSharedButtonStyle: GenerateStyle<ButtonToken, CSSObject> = (token): CSSObject => { const genSharedButtonStyle: GenerateStyle<ButtonToken, CSSObject> = (token): CSSObject => {
const { componentCls, iconCls } = token; const { componentCls, iconCls, fontWeight } = token;
return { return {
[componentCls]: { [componentCls]: {
outline: 'none', outline: 'none',
position: 'relative', position: 'relative',
display: 'inline-block', display: 'inline-block',
fontWeight: 400, fontWeight,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
textAlign: 'center', textAlign: 'center',
backgroundImage: 'none', backgroundImage: 'none',
backgroundColor: 'transparent', background: 'transparent',
border: `${token.lineWidth}px ${token.lineType} transparent`, border: `${unit(token.lineWidth)} ${token.lineType} transparent`,
cursor: 'pointer', cursor: 'pointer',
transition: `all ${token.motionDurationMid} ${token.motionEaseInOut}`, transition: `all ${token.motionDurationMid} ${token.motionEaseInOut}`,
userSelect: 'none', userSelect: 'none',
touchAction: 'manipulation', touchAction: 'manipulation',
lineHeight: token.lineHeight,
color: token.colorText, color: token.colorText,
'&:disabled > *': {
pointerEvents: 'none',
},
'> span': { '> span': {
display: 'inline-block', display: 'inline-block',
}, },
[`${componentCls}-icon`]: {
lineHeight: 0,
},
// Leave a space between icon and text. // Leave a space between icon and text.
[`> ${iconCls} + span, > span + ${iconCls}`]: { [`> ${iconCls} + span, > span + ${iconCls}`]: {
marginInlineStart: token.marginXS, marginInlineStart: token.marginXS,
}, },
[`&:not(${componentCls}-icon-only) > ${componentCls}-icon`]: {
[`&${componentCls}-loading-icon, &:not(:last-child)`]: {
marginInlineEnd: token.marginXS,
},
},
'> a': { '> a': {
color: 'currentColor', color: 'currentColor',
}, },
@ -54,54 +62,29 @@ const genSharedButtonStyle: GenerateStyle<ButtonToken, CSSObject> = (token): CSS
...genFocusStyle(token), ...genFocusStyle(token),
}, },
[`&${componentCls}-two-chinese-chars::first-letter`]: {
letterSpacing: '0.34em',
},
[`&${componentCls}-two-chinese-chars > *:not(${iconCls})`]: {
marginInlineEnd: '-0.34em',
letterSpacing: '0.34em',
},
// make `btn-icon-only` not too narrow // make `btn-icon-only` not too narrow
[`&-icon-only${componentCls}-compact-item`]: { [`&-icon-only${componentCls}-compact-item`]: {
flex: 'none', flex: 'none',
}, },
// Special styles for Primary Button
[`&-compact-item${componentCls}-primary`]: {
[`&:not([disabled]) + ${componentCls}-compact-item${componentCls}-primary:not([disabled])`]:
{
position: 'relative',
'&:before': {
position: 'absolute',
top: -token.lineWidth,
insetInlineStart: -token.lineWidth,
display: 'inline-block',
width: token.lineWidth,
height: `calc(100% + ${token.lineWidth * 2}px)`,
backgroundColor: token.colorPrimaryHover,
content: '""',
},
},
},
// Special styles for Primary Button
'&-compact-vertical-item': {
[`&${componentCls}-primary`]: {
[`&:not([disabled]) + ${componentCls}-compact-vertical-item${componentCls}-primary:not([disabled])`]:
{
position: 'relative',
'&:before': {
position: 'absolute',
top: -token.lineWidth,
insetInlineStart: -token.lineWidth,
display: 'inline-block',
width: `calc(100% + ${token.lineWidth * 2}px)`,
height: token.lineWidth,
backgroundColor: token.colorPrimaryHover,
content: '""',
},
},
},
},
}, },
}; } as CSSObject;
}; };
const genHoverActiveButtonStyle = (hoverStyle: CSSObject, activeStyle: CSSObject): CSSObject => ({ const genHoverActiveButtonStyle = (
'&:not(:disabled)': { btnCls: string,
hoverStyle: CSSObject,
activeStyle: CSSObject,
): CSSObject => ({
[`&:not(:disabled):not(${btnCls}-disabled)`]: {
'&:hover': hoverStyle, '&:hover': hoverStyle,
'&:active': activeStyle, '&:active': activeStyle,
}, },
@ -117,21 +100,22 @@ const genCircleButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
const genRoundButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({ const genRoundButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
borderRadius: token.controlHeight, borderRadius: token.controlHeight,
paddingInlineStart: token.controlHeight / 2, paddingInlineStart: token.calc(token.controlHeight).div(2).equal(),
paddingInlineEnd: token.controlHeight / 2, paddingInlineEnd: token.calc(token.controlHeight).div(2).equal(),
}); });
// =============================== Type =============================== // =============================== Type ===============================
const genDisabledStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({ const genDisabledStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
cursor: 'not-allowed', cursor: 'not-allowed',
borderColor: token.colorBorder, borderColor: token.borderColorDisabled,
color: token.colorTextDisabled, color: token.colorTextDisabled,
backgroundColor: token.colorBgContainerDisabled, background: token.colorBgContainerDisabled,
boxShadow: 'none', boxShadow: 'none',
}); });
const genGhostButtonStyle = ( const genGhostButtonStyle = (
btnCls: string, btnCls: string,
background: string,
textColor: string | false, textColor: string | false,
borderColor: string | false, borderColor: string | false,
textColorDisabled: string | false, textColorDisabled: string | false,
@ -141,17 +125,18 @@ const genGhostButtonStyle = (
): CSSObject => ({ ): CSSObject => ({
[`&${btnCls}-background-ghost`]: { [`&${btnCls}-background-ghost`]: {
color: textColor || undefined, color: textColor || undefined,
backgroundColor: 'transparent', background,
borderColor: borderColor || undefined, borderColor: borderColor || undefined,
boxShadow: 'none', boxShadow: 'none',
...genHoverActiveButtonStyle( ...genHoverActiveButtonStyle(
btnCls,
{ {
backgroundColor: 'transparent', background,
...hoverStyle, ...hoverStyle,
}, },
{ {
backgroundColor: 'transparent', background,
...activeStyle, ...activeStyle,
}, },
), ),
@ -165,7 +150,7 @@ const genGhostButtonStyle = (
}); });
const genSolidDisabledButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({ const genSolidDisabledButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
'&:disabled': { [`&:disabled, &${token.componentCls}-disabled`]: {
...genDisabledStyle(token), ...genDisabledStyle(token),
}, },
}); });
@ -175,7 +160,7 @@ const genSolidButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
}); });
const genPureDisabledButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({ const genPureDisabledButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
'&:disabled': { [`&:disabled, &${token.componentCls}-disabled`]: {
cursor: 'not-allowed', cursor: 'not-allowed',
color: token.colorTextDisabled, color: token.colorTextDisabled,
}, },
@ -185,12 +170,14 @@ const genPureDisabledButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token
const genDefaultButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({ const genDefaultButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
...genSolidButtonStyle(token), ...genSolidButtonStyle(token),
backgroundColor: token.colorBgContainer, background: token.defaultBg,
borderColor: token.colorBorder, borderColor: token.defaultBorderColor,
color: token.defaultColor,
boxShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlTmpOutline}`, boxShadow: token.defaultShadow,
...genHoverActiveButtonStyle( ...genHoverActiveButtonStyle(
token.componentCls,
{ {
color: token.colorPrimaryHover, color: token.colorPrimaryHover,
borderColor: token.colorPrimaryHover, borderColor: token.colorPrimaryHover,
@ -203,8 +190,9 @@ const genDefaultButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
...genGhostButtonStyle( ...genGhostButtonStyle(
token.componentCls, token.componentCls,
token.colorBgContainer, token.ghostBg,
token.colorBgContainer, token.defaultGhostColor,
token.defaultGhostBorderColor,
token.colorTextDisabled, token.colorTextDisabled,
token.colorBorder, token.colorBorder,
), ),
@ -214,6 +202,7 @@ const genDefaultButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
borderColor: token.colorError, borderColor: token.colorError,
...genHoverActiveButtonStyle( ...genHoverActiveButtonStyle(
token.componentCls,
{ {
color: token.colorErrorHover, color: token.colorErrorHover,
borderColor: token.colorErrorBorderHover, borderColor: token.colorErrorBorderHover,
@ -226,6 +215,7 @@ const genDefaultButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
...genGhostButtonStyle( ...genGhostButtonStyle(
token.componentCls, token.componentCls,
token.ghostBg,
token.colorError, token.colorError,
token.colorError, token.colorError,
token.colorTextDisabled, token.colorTextDisabled,
@ -239,24 +229,26 @@ const genDefaultButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
const genPrimaryButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({ const genPrimaryButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
...genSolidButtonStyle(token), ...genSolidButtonStyle(token),
color: token.colorTextLightSolid, color: token.primaryColor,
backgroundColor: token.colorPrimary, background: token.colorPrimary,
boxShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlOutline}`, boxShadow: token.primaryShadow,
...genHoverActiveButtonStyle( ...genHoverActiveButtonStyle(
token.componentCls,
{ {
color: token.colorTextLightSolid, color: token.colorTextLightSolid,
backgroundColor: token.colorPrimaryHover, background: token.colorPrimaryHover,
}, },
{ {
color: token.colorTextLightSolid, color: token.colorTextLightSolid,
backgroundColor: token.colorPrimaryActive, background: token.colorPrimaryActive,
}, },
), ),
...genGhostButtonStyle( ...genGhostButtonStyle(
token.componentCls, token.componentCls,
token.ghostBg,
token.colorPrimary, token.colorPrimary,
token.colorPrimary, token.colorPrimary,
token.colorTextDisabled, token.colorTextDisabled,
@ -272,20 +264,23 @@ const genPrimaryButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
), ),
[`&${token.componentCls}-dangerous`]: { [`&${token.componentCls}-dangerous`]: {
backgroundColor: token.colorError, background: token.colorError,
boxShadow: `0 ${token.controlOutlineWidth}px 0 ${token.colorErrorOutline}`, boxShadow: token.dangerShadow,
color: token.dangerColor,
...genHoverActiveButtonStyle( ...genHoverActiveButtonStyle(
token.componentCls,
{ {
backgroundColor: token.colorErrorHover, background: token.colorErrorHover,
}, },
{ {
backgroundColor: token.colorErrorActive, background: token.colorErrorActive,
}, },
), ),
...genGhostButtonStyle( ...genGhostButtonStyle(
token.componentCls, token.componentCls,
token.ghostBg,
token.colorError, token.colorError,
token.colorError, token.colorError,
token.colorTextDisabled, token.colorTextDisabled,
@ -314,8 +309,10 @@ const genLinkButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
color: token.colorLink, color: token.colorLink,
...genHoverActiveButtonStyle( ...genHoverActiveButtonStyle(
token.componentCls,
{ {
color: token.colorLinkHover, color: token.colorLinkHover,
background: token.linkHoverBg,
}, },
{ {
color: token.colorLinkActive, color: token.colorLinkActive,
@ -328,6 +325,7 @@ const genLinkButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
color: token.colorError, color: token.colorError,
...genHoverActiveButtonStyle( ...genHoverActiveButtonStyle(
token.componentCls,
{ {
color: token.colorErrorHover, color: token.colorErrorHover,
}, },
@ -343,13 +341,14 @@ const genLinkButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
// Type: Text // Type: Text
const genTextButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({ const genTextButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
...genHoverActiveButtonStyle( ...genHoverActiveButtonStyle(
token.componentCls,
{ {
color: token.colorText, color: token.colorText,
backgroundColor: token.colorBgTextHover, background: token.textHoverBg,
}, },
{ {
color: token.colorText, color: token.colorText,
backgroundColor: token.colorBgTextActive, background: token.colorBgTextActive,
}, },
), ),
@ -360,26 +359,19 @@ const genTextButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
...genPureDisabledButtonStyle(token), ...genPureDisabledButtonStyle(token),
...genHoverActiveButtonStyle( ...genHoverActiveButtonStyle(
token.componentCls,
{ {
color: token.colorErrorHover, color: token.colorErrorHover,
backgroundColor: token.colorErrorBg, background: token.colorErrorBg,
}, },
{ {
color: token.colorErrorHover, color: token.colorErrorHover,
backgroundColor: token.colorErrorBg, background: token.colorErrorBg,
}, },
), ),
}, },
}); });
// Href and Disabled
const genDisabledButtonStyle: GenerateStyle<ButtonToken, CSSObject> = token => ({
...genDisabledStyle(token),
[`&${token.componentCls}:hover`]: {
...genDisabledStyle(token),
},
});
const genTypeButtonStyle: GenerateStyle<ButtonToken> = token => { const genTypeButtonStyle: GenerateStyle<ButtonToken> = token => {
const { componentCls } = token; const { componentCls } = token;
@ -389,26 +381,30 @@ const genTypeButtonStyle: GenerateStyle<ButtonToken> = token => {
[`${componentCls}-dashed`]: genDashedButtonStyle(token), [`${componentCls}-dashed`]: genDashedButtonStyle(token),
[`${componentCls}-link`]: genLinkButtonStyle(token), [`${componentCls}-link`]: genLinkButtonStyle(token),
[`${componentCls}-text`]: genTextButtonStyle(token), [`${componentCls}-text`]: genTextButtonStyle(token),
[`${componentCls}-disabled`]: genDisabledButtonStyle(token), [`${componentCls}-ghost`]: genGhostButtonStyle(
token.componentCls,
token.ghostBg,
token.colorBgContainer,
token.colorBgContainer,
token.colorTextDisabled,
token.colorBorder,
),
}; };
}; };
// =============================== Size =============================== // =============================== Size ===============================
const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSSInterpolation => { const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = '') => {
const { const {
componentCls, componentCls,
iconCls,
controlHeight, controlHeight,
fontSize, fontSize,
lineHeight, lineHeight,
lineWidth,
borderRadius, borderRadius,
buttonPaddingHorizontal, buttonPaddingHorizontal,
iconCls,
buttonPaddingVertical,
} = token; } = token;
const paddingVertical = Math.max(0, (controlHeight - fontSize * lineHeight) / 2 - lineWidth);
const paddingHorizontal = buttonPaddingHorizontal - lineWidth;
const iconOnlyCls = `${componentCls}-icon-only`; const iconOnlyCls = `${componentCls}-icon-only`;
return [ return [
@ -416,8 +412,9 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS
{ {
[`${componentCls}${sizePrefixCls}`]: { [`${componentCls}${sizePrefixCls}`]: {
fontSize, fontSize,
lineHeight,
height: controlHeight, height: controlHeight,
padding: `${paddingVertical}px ${paddingHorizontal}px`, padding: `${unit(buttonPaddingVertical!)} ${unit(buttonPaddingHorizontal!)}`,
borderRadius, borderRadius,
[`&${iconOnlyCls}`]: { [`&${iconOnlyCls}`]: {
@ -427,8 +424,8 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS
[`&${componentCls}-round`]: { [`&${componentCls}-round`]: {
width: 'auto', width: 'auto',
}, },
'> span': { [iconCls]: {
transform: 'scale(1.143)', // 14px -> 16px fontSize: token.buttonIconOnlyFontSize,
}, },
}, },
@ -441,10 +438,6 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS
[`${componentCls}-loading-icon`]: { [`${componentCls}-loading-icon`]: {
transition: `width ${token.motionDurationSlow} ${token.motionEaseInOut}, opacity ${token.motionDurationSlow} ${token.motionEaseInOut}`, transition: `width ${token.motionDurationSlow} ${token.motionEaseInOut}, opacity ${token.motionDurationSlow} ${token.motionEaseInOut}`,
}, },
[`&:not(${iconOnlyCls}) ${componentCls}-loading-icon > ${iconCls}`]: {
marginInlineEnd: token.marginXS,
},
}, },
}, },
@ -458,14 +451,24 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS
]; ];
}; };
const genSizeBaseButtonStyle: GenerateStyle<ButtonToken> = token => genSizeButtonStyle(token); const genSizeBaseButtonStyle: GenerateStyle<ButtonToken> = token =>
genSizeButtonStyle(
mergeToken<ButtonToken>(token, {
fontSize: token.contentFontSize,
lineHeight: token.contentLineHeight,
}),
);
const genSizeSmallButtonStyle: GenerateStyle<ButtonToken> = token => { const genSizeSmallButtonStyle: GenerateStyle<ButtonToken> = token => {
const smallToken = mergeToken<ButtonToken>(token, { const smallToken = mergeToken<ButtonToken>(token, {
controlHeight: token.controlHeightSM, controlHeight: token.controlHeightSM,
fontSize: token.contentFontSizeSM,
lineHeight: token.contentLineHeightSM,
padding: token.paddingXS, padding: token.paddingXS,
buttonPaddingHorizontal: 8, // Fixed padding buttonPaddingHorizontal: token.paddingInlineSM,
buttonPaddingVertical: token.paddingBlockSM,
borderRadius: token.borderRadiusSM, borderRadius: token.borderRadiusSM,
buttonIconOnlyFontSize: token.onlyIconSizeSM,
}); });
return genSizeButtonStyle(smallToken, `${token.componentCls}-sm`); return genSizeButtonStyle(smallToken, `${token.componentCls}-sm`);
@ -474,8 +477,12 @@ const genSizeSmallButtonStyle: GenerateStyle<ButtonToken> = token => {
const genSizeLargeButtonStyle: GenerateStyle<ButtonToken> = token => { const genSizeLargeButtonStyle: GenerateStyle<ButtonToken> = token => {
const largeToken = mergeToken<ButtonToken>(token, { const largeToken = mergeToken<ButtonToken>(token, {
controlHeight: token.controlHeightLG, controlHeight: token.controlHeightLG,
fontSize: token.fontSizeLG, fontSize: token.contentFontSizeLG,
lineHeight: token.contentLineHeightLG,
buttonPaddingHorizontal: token.paddingInlineLG,
buttonPaddingVertical: token.paddingBlockLG,
borderRadius: token.borderRadiusLG, borderRadius: token.borderRadiusLG,
buttonIconOnlyFontSize: token.onlyIconSizeLG,
}); });
return genSizeButtonStyle(largeToken, `${token.componentCls}-lg`); return genSizeButtonStyle(largeToken, `${token.componentCls}-lg`);
@ -493,33 +500,37 @@ const genBlockButtonStyle: GenerateStyle<ButtonToken> = token => {
}; };
// ============================== Export ============================== // ============================== Export ==============================
export default genComponentStyleHook('Button', token => { export default genStyleHooks(
const { controlTmpOutline, paddingContentHorizontal } = token; 'Button',
const buttonToken = mergeToken<ButtonToken>(token, { token => {
colorOutlineDefault: controlTmpOutline, const buttonToken = prepareToken(token);
buttonPaddingHorizontal: paddingContentHorizontal,
});
return [ return [
// Shared // Shared
genSharedButtonStyle(buttonToken), genSharedButtonStyle(buttonToken),
// Size // Size
genSizeSmallButtonStyle(buttonToken), genSizeSmallButtonStyle(buttonToken),
genSizeBaseButtonStyle(buttonToken), genSizeBaseButtonStyle(buttonToken),
genSizeLargeButtonStyle(buttonToken), genSizeLargeButtonStyle(buttonToken),
// Block // Block
genBlockButtonStyle(buttonToken), genBlockButtonStyle(buttonToken),
// Group (type, ghost, danger, disabled, loading) // Group (type, ghost, danger, loading)
genTypeButtonStyle(buttonToken), genTypeButtonStyle(buttonToken),
// Button Group // Button Group
genGroupStyle(buttonToken), genGroupStyle(buttonToken),
];
// Space Compact },
genCompactItemStyle(token, { focus: false }), prepareComponentToken,
genCompactItemVerticalStyle(token), {
]; unitless: {
}); fontWeight: true,
contentLineHeight: true,
contentLineHeightSM: true,
contentLineHeightLG: true,
},
},
);

View File

@ -0,0 +1,234 @@
import type { CSSProperties } from 'vue';
import type { FullToken, GetDefaultToken } from '../../theme/internal';
import { getLineHeight, mergeToken } from '../../theme/internal';
import type { GenStyleFn } from '../../theme/util/genComponentStyleHook';
/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
/**
* @desc
* @descEN Font weight of text
*/
fontWeight: CSSProperties['fontWeight'];
/**
* @desc
* @descEN Shadow of default button
*/
defaultShadow: string;
/**
* @desc
* @descEN Shadow of primary button
*/
primaryShadow: string;
/**
* @desc
* @descEN Shadow of danger button
*/
dangerShadow: string;
/**
* @desc
* @descEN Text color of primary button
*/
primaryColor: string;
/**
* @desc
* @descEN Text color of default button
*/
defaultColor: string;
/**
* @desc
* @descEN Background color of default button
*/
defaultBg: string;
/**
* @desc
* @descEN Border color of default button
*/
defaultBorderColor: string;
/**
* @desc
* @descEN Text color of danger button
*/
dangerColor: string;
/**
* @desc
* @descEN Border color of disabled button
*/
borderColorDisabled: string;
/**
* @desc
* @descEN Text color of default ghost button
*/
defaultGhostColor: string;
/**
* @desc
* @descEN Background color of ghost button
*/
ghostBg: string;
/**
* @desc
* @descEN Border color of default ghost button
*/
defaultGhostBorderColor: string;
/**
* @desc
* @descEN Horizontal padding of button
*/
paddingInline: CSSProperties['paddingInline'];
/**
* @desc
* @descEN Horizontal padding of large button
*/
paddingInlineLG: CSSProperties['paddingInline'];
/**
* @desc
* @descEN Horizontal padding of small button
*/
paddingInlineSM: CSSProperties['paddingInline'];
/**
* @desc
* @descEN Horizontal padding of button
*/
paddingBlock: CSSProperties['paddingInline'];
/**
* @desc
* @descEN Horizontal padding of large button
*/
paddingBlockLG: CSSProperties['paddingInline'];
/**
* @desc
* @descEN Horizontal padding of small button
*/
paddingBlockSM: CSSProperties['paddingInline'];
/**
* @desc
* @descEN Icon size of button which only contains icon
*/
onlyIconSize: number;
/**
* @desc
* @descEN Icon size of large button which only contains icon
*/
onlyIconSizeLG: number;
/**
* @desc
* @descEN Icon size of small button which only contains icon
*/
onlyIconSizeSM: number;
/**
* @desc
* @descEN Border color of button group
*/
groupBorderColor: string;
/**
* @desc
* @descEN Background color of link button when hover
*/
linkHoverBg: string;
/**
* @desc
* @descEN Background color of text button when hover
*/
textHoverBg: string;
/**
* @desc
* @descEN Font size of button content
*/
contentFontSize: number;
/**
* @desc
* @descEN Font size of large button content
*/
contentFontSizeLG: number;
/**
* @desc
* @descEN Font size of small button content
*/
contentFontSizeSM: number;
/**
* @desc
* @descEN Line height of button content
*/
contentLineHeight: number;
/**
* @desc
* @descEN Line height of large button content
*/
contentLineHeightLG: number;
/**
* @desc
* @descEN Line height of small button content
*/
contentLineHeightSM: number;
}
export interface ButtonToken extends FullToken<'Button'> {
buttonPaddingHorizontal: CSSProperties['paddingInline'];
buttonPaddingVertical: CSSProperties['paddingBlock'];
buttonIconOnlyFontSize: number;
}
export const prepareToken: (token: Parameters<GenStyleFn<'Button'>>[0]) => ButtonToken = token => {
const { paddingInline, onlyIconSize, paddingBlock } = token;
const buttonToken = mergeToken<ButtonToken>(token, {
buttonPaddingHorizontal: paddingInline,
buttonPaddingVertical: paddingBlock,
buttonIconOnlyFontSize: onlyIconSize,
});
return buttonToken;
};
export const prepareComponentToken: GetDefaultToken<'Button'> = token => {
const contentFontSize = token.contentFontSize ?? token.fontSize;
const contentFontSizeSM = token.contentFontSizeSM ?? token.fontSize;
const contentFontSizeLG = token.contentFontSizeLG ?? token.fontSizeLG;
const contentLineHeight = token.contentLineHeight ?? getLineHeight(contentFontSize);
const contentLineHeightSM = token.contentLineHeightSM ?? getLineHeight(contentFontSizeSM);
const contentLineHeightLG = token.contentLineHeightLG ?? getLineHeight(contentFontSizeLG);
return {
fontWeight: 400,
defaultShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlTmpOutline}`,
primaryShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlOutline}`,
dangerShadow: `0 ${token.controlOutlineWidth}px 0 ${token.colorErrorOutline}`,
primaryColor: token.colorTextLightSolid,
dangerColor: token.colorTextLightSolid,
borderColorDisabled: token.colorBorder,
defaultGhostColor: token.colorBgContainer,
ghostBg: 'transparent',
defaultGhostBorderColor: token.colorBgContainer,
paddingInline: token.paddingContentHorizontal - token.lineWidth,
paddingInlineLG: token.paddingContentHorizontal - token.lineWidth,
paddingInlineSM: 8 - token.lineWidth,
onlyIconSize: token.fontSizeLG,
onlyIconSizeSM: token.fontSizeLG - 2,
onlyIconSizeLG: token.fontSizeLG + 2,
groupBorderColor: token.colorPrimaryHover,
linkHoverBg: 'transparent',
textHoverBg: token.colorBgTextHover,
defaultColor: token.colorText,
defaultBg: token.colorBgContainer,
defaultBorderColor: token.colorBorder,
defaultBorderColorDisabled: token.colorBorder,
contentFontSize,
contentFontSizeSM,
contentFontSizeLG,
contentLineHeight,
contentLineHeightSM,
contentLineHeightLG,
paddingBlock: Math.max(
(token.controlHeight - contentFontSize * contentLineHeight) / 2 - token.lineWidth,
0,
),
paddingBlockSM: Math.max(
(token.controlHeightSM - contentFontSizeSM * contentLineHeightSM) / 2 - token.lineWidth,
0,
),
paddingBlockLG: Math.max(
(token.controlHeightLG - contentFontSizeLG * contentLineHeightLG) / 2 - token.lineWidth,
0,
),
};
};

View File

@ -57,6 +57,18 @@ export interface ThemeConfig {
algorithm?: MappingAlgorithm | MappingAlgorithm[]; algorithm?: MappingAlgorithm | MappingAlgorithm[];
hashed?: boolean; hashed?: boolean;
inherit?: boolean; inherit?: boolean;
cssVar?:
| {
/**
* Prefix for css variable, default to `antd`.
*/
prefix?: string;
/**
* Unique key for theme, should be set manually < react@18.
*/
key?: string;
}
| boolean;
} }
export const configProviderProps = () => ({ export const configProviderProps = () => ({

View File

@ -0,0 +1,16 @@
import { useToken } from '../../theme/internal';
import type { Ref } from 'vue';
import { computed } from 'vue';
/**
* This hook is only for cssVar to add root className for components.
* If root ClassName is needed, this hook could be refactored with `-root`
* @param prefixCls
*/
const useCSSVarCls = (prefixCls: Ref<string>) => {
const [, , , , cssVar] = useToken();
return computed(() => (cssVar.value ? `${prefixCls.value}-css-var` : ''));
};
export default useCSSVarCls;

View File

@ -0,0 +1,32 @@
import type { SizeType } from '../SizeContext';
import { useInjectSize } from '../SizeContext';
import type { Ref } from 'vue';
import { computed, shallowRef, watch } from 'vue';
const useSize = <T>(customSize?: T | ((ctxSize: SizeType) => T)): Ref<T> => {
const size = useInjectSize();
const mergedSize = shallowRef(null);
watch(
computed(() => {
return [customSize, size.value];
}),
() => {
if (!customSize) {
mergedSize.value = size.value as T;
}
if (typeof customSize === 'string') {
mergedSize.value = customSize ?? (size.value as T);
}
if (customSize instanceof Function) {
mergedSize.value = customSize(size.value) as T;
}
},
{ immediate: true },
);
return mergedSize;
};
export default useSize;

View File

@ -2,13 +2,26 @@ import type { ThemeConfig } from '../context';
import { defaultConfig } from '../../theme/internal'; import { defaultConfig } from '../../theme/internal';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed } from 'vue'; import { computed } from 'vue';
import devWarning from '../../vc-util/warning';
const themeKey = 'antdvtheme';
export default function useTheme(theme?: Ref<ThemeConfig>, parentTheme?: Ref<ThemeConfig>) { export default function useTheme(theme?: Ref<ThemeConfig>, parentTheme?: Ref<ThemeConfig>) {
const themeConfig = computed(() => theme?.value || {}); const themeConfig = computed(() => theme?.value || {});
const parentThemeConfig = computed<ThemeConfig>(() => const parentThemeConfig = computed<ThemeConfig>(() =>
themeConfig.value.inherit === false || !parentTheme?.value ? defaultConfig : parentTheme.value, themeConfig.value.inherit === false || !parentTheme?.value ? defaultConfig : parentTheme.value,
); );
if (process.env.NODE_ENV !== 'production') {
const cssVarEnabled = themeConfig.value.cssVar || parentThemeConfig.value.cssVar;
const validKey = !!(
(typeof themeConfig.value.cssVar === 'object' && themeConfig.value.cssVar?.key) ||
themeKey
);
devWarning(
!cssVarEnabled || validKey,
'[Ant Design Vue ConfigProvider] Missing key in `cssVar` config. Please set `cssVar.key` manually in each ConfigProvider inside `cssVar` enabled ConfigProvider.',
);
}
const mergedTheme = computed(() => { const mergedTheme = computed(() => {
if (!theme?.value) { if (!theme?.value) {
return parentTheme?.value; return parentTheme?.value;
@ -26,6 +39,17 @@ export default function useTheme(theme?: Ref<ThemeConfig>, parentTheme?: Ref<The
} as any; } as any;
}); });
const cssVarKey = `css-var-${themeKey.replace(/:/g, '')}`;
const mergedCssVar = (themeConfig.value.cssVar ?? parentThemeConfig.value.cssVar) && {
prefix: 'ant', // Default to ant
...(typeof parentThemeConfig.value.cssVar === 'object' ? parentThemeConfig.value.cssVar : {}),
...(typeof themeConfig.value.cssVar === 'object' ? themeConfig.value.cssVar : {}),
key:
(typeof themeConfig.value.cssVar === 'object' && themeConfig.value.cssVar?.key) ||
cssVarKey,
};
// Base token // Base token
return { return {
...parentThemeConfig.value, ...parentThemeConfig.value,
@ -36,6 +60,7 @@ export default function useTheme(theme?: Ref<ThemeConfig>, parentTheme?: Ref<The
...themeConfig.value.token, ...themeConfig.value.token,
}, },
components: mergedComponents, components: mergedComponents,
cssVar: mergedCssVar,
}; };
}); });

View File

@ -0,0 +1,6 @@
let uid = 0;
const useThemeKey = () => {
return 'themekey' + uid++;
};
export default useThemeKey;

View File

@ -15,7 +15,7 @@ import type { ValidateMessages } from '../form/interface';
import useStyle from './style'; import useStyle from './style';
import useTheme from './hooks/useTheme'; import useTheme from './hooks/useTheme';
import defaultSeedToken from '../theme/themes/seed'; import defaultSeedToken from '../theme/themes/seed';
import type { ConfigProviderInnerProps, ConfigProviderProps, Theme } from './context'; import type { ConfigProviderInnerProps, ConfigProviderProps, Theme, ThemeConfig } from './context';
import { import {
useConfigContextProvider, useConfigContextProvider,
useConfigContextInject, useConfigContextInject,
@ -26,7 +26,7 @@ import {
import { useProviderSize } from './SizeContext'; import { useProviderSize } from './SizeContext';
import { useProviderDisabled } from './DisabledContext'; import { useProviderDisabled } from './DisabledContext';
import { createTheme } from '../_util/cssinjs'; import { createTheme } from '../_util/cssinjs';
import { DesignTokenProvider } from '../theme/internal'; import { defaultTheme, DesignTokenProvider } from '../theme/context';
export type { export type {
ConfigProviderProps, ConfigProviderProps,
@ -226,19 +226,47 @@ const ConfigProvider = defineComponent({
// ================================ Dynamic theme ================================ // ================================ Dynamic theme ================================
const memoTheme = computed(() => { const memoTheme = computed(() => {
const { algorithm, token, ...rest } = mergedTheme.value || {}; const { algorithm, token, components, cssVar, ...rest } = mergedTheme.value || {};
const themeObj = const themeObj =
algorithm && (!Array.isArray(algorithm) || algorithm.length > 0) algorithm && (!Array.isArray(algorithm) || algorithm.length > 0)
? createTheme(algorithm) ? createTheme(algorithm)
: undefined; : defaultTheme;
const parsedComponents: any = {};
Object.entries(components || {}).forEach(([componentName, componentToken]) => {
const parsedToken: typeof componentToken & { theme?: typeof defaultTheme } = {
...componentToken,
};
if ('algorithm' in parsedToken) {
if (parsedToken.algorithm === true) {
parsedToken.theme = themeObj;
} else if (
Array.isArray(parsedToken.algorithm) ||
typeof parsedToken.algorithm === 'function'
) {
parsedToken.theme = createTheme(parsedToken.algorithm as any);
}
delete parsedToken.algorithm;
}
parsedComponents[componentName] = parsedToken;
});
const mergedToken = {
...defaultSeedToken,
...token,
};
return { return {
...rest, ...rest,
theme: themeObj, theme: themeObj,
token: { token: mergedToken,
...defaultSeedToken, components: parsedComponents,
...token, override: {
override: mergedToken,
...parsedComponents,
}, },
cssVar: cssVar as Exclude<ThemeConfig['cssVar'], boolean>,
}; };
}); });
const validateMessagesRef = computed(() => { const validateMessagesRef = computed(() => {

View File

@ -1,3 +1,4 @@
import type { CSSObject } from '../../_util/cssinjs';
import { useStyleRegister } from '../../_util/cssinjs'; import { useStyleRegister } from '../../_util/cssinjs';
import { resetIcon } from '../../style'; import { resetIcon } from '../../style';
import { useToken } from '../../theme/internal'; import { useToken } from '../../theme/internal';
@ -13,16 +14,17 @@ const useStyle = (iconPrefixCls: Ref<string>) => {
hashId: '', hashId: '',
path: ['ant-design-icons', iconPrefixCls.value], path: ['ant-design-icons', iconPrefixCls.value],
})), })),
() => [ () =>
{ [
[`.${iconPrefixCls.value}`]: { {
...resetIcon(), [`.${iconPrefixCls.value}`]: {
[`.${iconPrefixCls.value} .${iconPrefixCls.value}-icon`]: { ...resetIcon(),
display: 'block', [`.${iconPrefixCls.value} .${iconPrefixCls.value}-icon`]: {
display: 'block',
},
}, },
}, },
}, ] as CSSObject[],
],
); );
}; };

View File

@ -22,6 +22,8 @@ import type { TokenWithCommonCls } from '../../theme/util/genComponentStyleHook'
import { resetComponent, roundedArrow, textEllipsis } from '../../style'; import { resetComponent, roundedArrow, textEllipsis } from '../../style';
import { genCompactItemStyle } from '../../style/compact-item'; import { genCompactItemStyle } from '../../style/compact-item';
export interface ComponentToken {}
export interface ComponentToken { export interface ComponentToken {
presetsWidth: number; presetsWidth: number;
presetsMaxWidth: number; presetsMaxWidth: number;

View File

@ -3,6 +3,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { resetComponent, textEllipsis } from '../../style'; import { resetComponent, textEllipsis } from '../../style';
export interface ComponentToken {}
interface DescriptionsToken extends FullToken<'Descriptions'> { interface DescriptionsToken extends FullToken<'Descriptions'> {
descriptionsTitleMarginBottom: number; descriptionsTitleMarginBottom: number;
descriptionsExtraColor: string; descriptionsExtraColor: string;

View File

@ -60,7 +60,7 @@ const BackTop = defineComponent({
const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => { const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => {
const { visibilityHeight } = props; const { visibilityHeight } = props;
const scrollTop = getScroll(e.target, true); const scrollTop = getScroll(e.target);
state.visible = scrollTop >= visibilityHeight; state.visible = scrollTop >= visibilityHeight;
}); });

View File

@ -5,6 +5,8 @@ import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { resetComponent } from '../../style'; import { resetComponent } from '../../style';
import genFormValidateMotionStyle from './explain'; import genFormValidateMotionStyle from './explain';
export interface ComponentToken {}
export interface FormToken extends FullToken<'Form'> { export interface FormToken extends FullToken<'Form'> {
formItemCls: string; formItemCls: string;
rootPrefixCls: string; rootPrefixCls: string;

View File

@ -2,6 +2,8 @@ import type { CSSObject } from '../../_util/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal'; import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal';
export interface ComponentToken {}
interface GridRowToken extends FullToken<'Grid'> {} interface GridRowToken extends FullToken<'Grid'> {}
interface GridColToken extends FullToken<'Grid'> { interface GridColToken extends FullToken<'Grid'> {

View File

@ -2,7 +2,7 @@ import type { App } from 'vue';
import * as components from './components'; import * as components from './components';
import { default as version } from './version'; import { default as version } from './version';
import cssinjs from './_util/cssinjs'; import * as cssinjs from './_util/cssinjs';
export * from './components'; export * from './components';
export * from './_util/cssinjs'; export * from './_util/cssinjs';

View File

@ -5,6 +5,8 @@ import type { GlobalToken } from '../../theme/interface';
import { clearFix, resetComponent } from '../../style'; import { clearFix, resetComponent } from '../../style';
import { genCompactItemStyle } from '../../style/compact-item'; import { genCompactItemStyle } from '../../style/compact-item';
export interface ComponentToken {}
export type InputToken<T extends GlobalToken = FullToken<'Input'>> = T & { export type InputToken<T extends GlobalToken = FullToken<'Input'>> = T & {
inputAffixPadding: number; inputAffixPadding: number;
inputPaddingVertical: number; inputPaddingVertical: number;

View File

@ -4,6 +4,8 @@ import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { resetComponent, textEllipsis } from '../../style'; import { resetComponent, textEllipsis } from '../../style';
import { operationUnit } from '../../style'; import { operationUnit } from '../../style';
export interface ComponentToken {}
interface PageHeaderToken extends FullToken<'PageHeader'> { interface PageHeaderToken extends FullToken<'PageHeader'> {
pageHeaderPadding: number; pageHeaderPadding: number;
pageHeaderPaddingVertical: number; pageHeaderPaddingVertical: number;

View File

@ -9,6 +9,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { genFocusOutline, genFocusStyle, resetComponent } from '../../style'; import { genFocusOutline, genFocusStyle, resetComponent } from '../../style';
export interface ComponentToken {}
interface PaginationToken extends InputToken<FullToken<'Pagination'>> { interface PaginationToken extends InputToken<FullToken<'Pagination'>> {
paginationItemSize: number; paginationItemSize: number;
paginationFontFamily: string; paginationFontFamily: string;

View File

@ -3,6 +3,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { resetComponent } from '../../style'; import { resetComponent } from '../../style';
export interface ComponentToken {}
interface StatisticToken extends FullToken<'Statistic'> { interface StatisticToken extends FullToken<'Statistic'> {
statisticTitleFontSize: number; statisticTitleFontSize: number;
statisticContentFontSize: number; statisticContentFontSize: number;

View File

@ -19,7 +19,8 @@ The most basic usage.
<template> <template>
<a-switch v-model:checked="checked" /> <a-switch v-model:checked="checked" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
const checked = ref<boolean>(false); const checked = ref<boolean>(true);
</script> </script>

View File

@ -27,6 +27,6 @@ title:
import { reactive } from 'vue'; import { reactive } from 'vue';
const state = reactive({ const state = reactive({
checked1: true, checked1: true,
checked2: false, checked2: true,
}); });
</script> </script>

View File

@ -18,7 +18,7 @@ With text and icon.
<template> <template>
<a-space direction="vertical"> <a-space direction="vertical">
<a-switch v-model:checked="state.checked1" checked-children="" un-checked-children="" /> <a-switch v-model:checked="state.checked1" checked-children="" un-checked-children="" />
<a-switch v-model:checked="state.checked2" checked-children="1" un-checked-children="0" /> <a-switch v-model:checked="state.checked2" checked-children="1" un-checked-children="0" />
<a-switch v-model:checked="state.checked3"> <a-switch v-model:checked="state.checked3">
<template #checkedChildren><check-outlined /></template> <template #checkedChildren><check-outlined /></template>
@ -32,6 +32,6 @@ import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
const state = reactive({ const state = reactive({
checked1: true, checked1: true,
checked2: false, checked2: false,
checked3: false, checked3: true,
}); });
</script> </script>

View File

@ -98,7 +98,7 @@ const Switch = defineComponent({
); );
const { prefixCls, direction, size } = useConfigInject('switch', props); const { prefixCls, direction, size } = useConfigInject('switch', props);
const [wrapSSR, hashId] = useStyle(prefixCls); const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const refSwitchNode = ref(); const refSwitchNode = ref();
const focus = () => { const focus = () => {
refSwitchNode.value?.focus(); refSwitchNode.value?.focus();
@ -159,10 +159,11 @@ const Switch = defineComponent({
[prefixCls.value]: true, [prefixCls.value]: true,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl', [`${prefixCls.value}-rtl`]: direction.value === 'rtl',
[hashId.value]: true, [hashId.value]: true,
[cssVarCls.value]: true,
})); }));
return () => return () =>
wrapSSR( wrapCSSVar(
<Wave> <Wave>
<button <button
{...omit(props, [ {...omit(props, [

View File

@ -1,107 +1,172 @@
import type { CSSObject } from '../../_util/cssinjs'; import type { CSSObject } from '../../_util/cssinjs';
import { unit } from '../../_util/cssinjs';
import { TinyColor } from '@ctrl/tinycolor'; import { TinyColor } from '@ctrl/tinycolor';
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { genFocusStyle, resetComponent } from '../../style'; import { genFocusStyle, resetComponent } from '../../style';
import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal';
export interface ComponentToken {
/**
* @desc
* @descEN Height of Switch
*/
trackHeight: number | string;
/**
* @desc
* @descEN Height of small Switch
*/
trackHeightSM: number | string;
/**
* @desc
* @descEN Minimum width of Switch
*/
trackMinWidth: number | string;
/**
* @desc
* @descEN Minimum width of small Switch
*/
trackMinWidthSM: number | string;
/**
* @desc
* @descEN Padding of Switch
*/
trackPadding: number;
/**
* @desc
* @descEN Background color of Switch handle
*/
handleBg: string;
/**
* @desc
* @descEN Shadow of Switch handle
*/
handleShadow: string;
/**
* @desc
* @descEN Size of Switch handle
*/
handleSize: number;
/**
* @desc
* @descEN Size of small Switch handle
*/
handleSizeSM: number;
/**
* @desc
* @descEN Minimum margin of content area
*/
innerMinMargin: number;
/**
* @desc
* @descEN Maximum margin of content area
*/
innerMaxMargin: number;
/**
* @desc
* @descEN Minimum margin of content area of small Switch
*/
innerMinMarginSM: number;
/**
* @desc
* @descEN Maximum margin of content area of small Switch
*/
innerMaxMarginSM: number;
}
interface SwitchToken extends FullToken<'Switch'> { interface SwitchToken extends FullToken<'Switch'> {
switchMinWidth: number;
switchHeight: number;
switchDuration: string; switchDuration: string;
switchColor: string; switchColor: string;
switchDisabledOpacity: number; switchDisabledOpacity: number;
switchInnerMarginMin: number; switchLoadingIconSize: number | string;
switchInnerMarginMax: number;
switchPadding: number;
switchPinSize: number;
switchBg: string;
switchMinWidthSM: number;
switchHeightSM: number;
switchInnerMarginMinSM: number;
switchInnerMarginMaxSM: number;
switchPinSizeSM: number;
switchHandleShadow: string;
switchLoadingIconSize: number;
switchLoadingIconColor: string; switchLoadingIconColor: string;
switchHandleActiveInset: string; switchHandleActiveInset: string;
} }
const genSwitchSmallStyle: GenerateStyle<SwitchToken, CSSObject> = token => { const genSwitchSmallStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
const { componentCls } = token; const {
componentCls,
trackHeightSM,
trackPadding,
trackMinWidthSM,
innerMinMarginSM,
innerMaxMarginSM,
handleSizeSM,
calc,
} = token;
const switchInnerCls = `${componentCls}-inner`; const switchInnerCls = `${componentCls}-inner`;
const trackPaddingCalc = unit(calc(handleSizeSM).add(calc(trackPadding).mul(2)).equal());
const innerMaxMarginCalc = unit(calc(innerMaxMarginSM).mul(2).equal());
return { return {
[componentCls]: { [componentCls]: {
[`&${componentCls}-small`]: { [`&${componentCls}-small`]: {
minWidth: token.switchMinWidthSM, minWidth: trackMinWidthSM,
height: token.switchHeightSM, height: trackHeightSM,
lineHeight: `${token.switchHeightSM}px`, lineHeight: unit(trackHeightSM),
[`${componentCls}-inner`]: { [`${componentCls}-inner`]: {
paddingInlineStart: token.switchInnerMarginMaxSM, paddingInlineStart: innerMaxMarginSM,
paddingInlineEnd: token.switchInnerMarginMinSM, paddingInlineEnd: innerMinMarginSM,
[`${switchInnerCls}-checked, ${switchInnerCls}-unchecked`]: {
minHeight: trackHeightSM,
},
[`${switchInnerCls}-checked`]: { [`${switchInnerCls}-checked`]: {
marginInlineStart: `calc(-100% + ${ marginInlineStart: `calc(-100% + ${trackPaddingCalc} - ${innerMaxMarginCalc})`,
token.switchPinSizeSM + token.switchPadding * 2 marginInlineEnd: `calc(100% - ${trackPaddingCalc} + ${innerMaxMarginCalc})`,
}px - ${token.switchInnerMarginMaxSM * 2}px)`,
marginInlineEnd: `calc(100% - ${token.switchPinSizeSM + token.switchPadding * 2}px + ${
token.switchInnerMarginMaxSM * 2
}px)`,
}, },
[`${switchInnerCls}-unchecked`]: { [`${switchInnerCls}-unchecked`]: {
marginTop: -token.switchHeightSM, marginTop: calc(trackHeightSM).mul(-1).equal(),
marginInlineStart: 0, marginInlineStart: 0,
marginInlineEnd: 0, marginInlineEnd: 0,
}, },
}, },
[`${componentCls}-handle`]: { [`${componentCls}-handle`]: {
width: token.switchPinSizeSM, width: handleSizeSM,
height: token.switchPinSizeSM, height: handleSizeSM,
}, },
[`${componentCls}-loading-icon`]: { [`${componentCls}-loading-icon`]: {
top: (token.switchPinSizeSM - token.switchLoadingIconSize) / 2, top: calc(calc(handleSizeSM).sub(token.switchLoadingIconSize)).div(2).equal(),
fontSize: token.switchLoadingIconSize, fontSize: token.switchLoadingIconSize,
}, },
[`&${componentCls}-checked`]: { [`&${componentCls}-checked`]: {
[`${componentCls}-inner`]: { [`${componentCls}-inner`]: {
paddingInlineStart: token.switchInnerMarginMinSM, paddingInlineStart: innerMinMarginSM,
paddingInlineEnd: token.switchInnerMarginMaxSM, paddingInlineEnd: innerMaxMarginSM,
[`${switchInnerCls}-checked`]: { [`${switchInnerCls}-checked`]: {
marginInlineStart: 0, marginInlineStart: 0,
marginInlineEnd: 0, marginInlineEnd: 0,
}, },
[`${switchInnerCls}-unchecked`]: { [`${switchInnerCls}-unchecked`]: {
marginInlineStart: `calc(100% - ${ marginInlineStart: `calc(100% - ${trackPaddingCalc} + ${innerMaxMarginCalc})`,
token.switchPinSizeSM + token.switchPadding * 2 marginInlineEnd: `calc(-100% + ${trackPaddingCalc} - ${innerMaxMarginCalc})`,
}px + ${token.switchInnerMarginMaxSM * 2}px)`,
marginInlineEnd: `calc(-100% + ${
token.switchPinSizeSM + token.switchPadding * 2
}px - ${token.switchInnerMarginMaxSM * 2}px)`,
}, },
}, },
[`${componentCls}-handle`]: { [`${componentCls}-handle`]: {
insetInlineStart: `calc(100% - ${token.switchPinSizeSM + token.switchPadding}px)`, insetInlineStart: `calc(100% - ${unit(calc(handleSizeSM).add(trackPadding).equal())})`,
}, },
}, },
[`&:not(${componentCls}-disabled):active`]: { [`&:not(${componentCls}-disabled):active`]: {
[`&:not(${componentCls}-checked) ${switchInnerCls}`]: { [`&:not(${componentCls}-checked) ${switchInnerCls}`]: {
[`${switchInnerCls}-unchecked`]: { [`${switchInnerCls}-unchecked`]: {
marginInlineStart: token.marginXXS / 2, marginInlineStart: calc(token.marginXXS).div(2).equal(),
marginInlineEnd: -token.marginXXS / 2, marginInlineEnd: calc(token.marginXXS).mul(-1).div(2).equal(),
}, },
}, },
[`&${componentCls}-checked ${switchInnerCls}`]: { [`&${componentCls}-checked ${switchInnerCls}`]: {
[`${switchInnerCls}-checked`]: { [`${switchInnerCls}-checked`]: {
marginInlineStart: -token.marginXXS / 2, marginInlineStart: calc(token.marginXXS).mul(-1).div(2).equal(),
marginInlineEnd: token.marginXXS / 2, marginInlineEnd: calc(token.marginXXS).div(2).equal(),
}, },
}, },
}, },
@ -111,13 +176,13 @@ const genSwitchSmallStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
}; };
const genSwitchLoadingStyle: GenerateStyle<SwitchToken, CSSObject> = token => { const genSwitchLoadingStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
const { componentCls } = token; const { componentCls, handleSize, calc } = token;
return { return {
[componentCls]: { [componentCls]: {
[`${componentCls}-loading-icon${token.iconCls}`]: { [`${componentCls}-loading-icon${token.iconCls}`]: {
position: 'relative', position: 'relative',
top: (token.switchPinSize - token.fontSize) / 2, top: calc(calc(handleSize).sub(token.fontSize)).div(2).equal(),
color: token.switchLoadingIconColor, color: token.switchLoadingIconColor,
verticalAlign: 'top', verticalAlign: 'top',
}, },
@ -130,17 +195,17 @@ const genSwitchLoadingStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
}; };
const genSwitchHandleStyle: GenerateStyle<SwitchToken, CSSObject> = token => { const genSwitchHandleStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
const { componentCls } = token; const { componentCls, trackPadding, handleBg, handleShadow, handleSize, calc } = token;
const switchHandleCls = `${componentCls}-handle`; const switchHandleCls = `${componentCls}-handle`;
return { return {
[componentCls]: { [componentCls]: {
[switchHandleCls]: { [switchHandleCls]: {
position: 'absolute', position: 'absolute',
top: token.switchPadding, top: trackPadding,
insetInlineStart: token.switchPadding, insetInlineStart: trackPadding,
width: token.switchPinSize, width: handleSize,
height: token.switchPinSize, height: handleSize,
transition: `all ${token.switchDuration} ease-in-out`, transition: `all ${token.switchDuration} ease-in-out`,
'&::before': { '&::before': {
@ -149,16 +214,16 @@ const genSwitchHandleStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
insetInlineEnd: 0, insetInlineEnd: 0,
bottom: 0, bottom: 0,
insetInlineStart: 0, insetInlineStart: 0,
backgroundColor: token.colorWhite, backgroundColor: handleBg,
borderRadius: token.switchPinSize / 2, borderRadius: calc(handleSize).div(2).equal(),
boxShadow: token.switchHandleShadow, boxShadow: handleShadow,
transition: `all ${token.switchDuration} ease-in-out`, transition: `all ${token.switchDuration} ease-in-out`,
content: '""', content: '""',
}, },
}, },
[`&${componentCls}-checked ${switchHandleCls}`]: { [`&${componentCls}-checked ${switchHandleCls}`]: {
insetInlineStart: `calc(100% - ${token.switchPinSize + token.switchPadding}px)`, insetInlineStart: `calc(100% - ${unit(calc(handleSize).add(trackPadding).equal())})`,
}, },
[`&:not(${componentCls}-disabled):active`]: { [`&:not(${componentCls}-disabled):active`]: {
@ -177,9 +242,20 @@ const genSwitchHandleStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
}; };
const genSwitchInnerStyle: GenerateStyle<SwitchToken, CSSObject> = token => { const genSwitchInnerStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
const { componentCls } = token; const {
componentCls,
trackHeight,
trackPadding,
innerMinMargin,
innerMaxMargin,
handleSize,
calc,
} = token;
const switchInnerCls = `${componentCls}-inner`; const switchInnerCls = `${componentCls}-inner`;
const trackPaddingCalc = unit(calc(handleSize).add(calc(trackPadding).mul(2)).equal());
const innerMaxMarginCalc = unit(calc(innerMaxMargin).mul(2).equal());
return { return {
[componentCls]: { [componentCls]: {
[switchInnerCls]: { [switchInnerCls]: {
@ -187,8 +263,8 @@ const genSwitchInnerStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
overflow: 'hidden', overflow: 'hidden',
borderRadius: 100, borderRadius: 100,
height: '100%', height: '100%',
paddingInlineStart: token.switchInnerMarginMax, paddingInlineStart: innerMaxMargin,
paddingInlineEnd: token.switchInnerMarginMin, paddingInlineEnd: innerMinMargin,
transition: `padding-inline-start ${token.switchDuration} ease-in-out, padding-inline-end ${token.switchDuration} ease-in-out`, transition: `padding-inline-start ${token.switchDuration} ease-in-out, padding-inline-end ${token.switchDuration} ease-in-out`,
[`${switchInnerCls}-checked, ${switchInnerCls}-unchecked`]: { [`${switchInnerCls}-checked, ${switchInnerCls}-unchecked`]: {
@ -197,54 +273,47 @@ const genSwitchInnerStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
fontSize: token.fontSizeSM, fontSize: token.fontSizeSM,
transition: `margin-inline-start ${token.switchDuration} ease-in-out, margin-inline-end ${token.switchDuration} ease-in-out`, transition: `margin-inline-start ${token.switchDuration} ease-in-out, margin-inline-end ${token.switchDuration} ease-in-out`,
pointerEvents: 'none', pointerEvents: 'none',
minHeight: trackHeight,
}, },
[`${switchInnerCls}-checked`]: { [`${switchInnerCls}-checked`]: {
marginInlineStart: `calc(-100% + ${token.switchPinSize + token.switchPadding * 2}px - ${ marginInlineStart: `calc(-100% + ${trackPaddingCalc} - ${innerMaxMarginCalc})`,
token.switchInnerMarginMax * 2 marginInlineEnd: `calc(100% - ${trackPaddingCalc} + ${innerMaxMarginCalc})`,
}px)`,
marginInlineEnd: `calc(100% - ${token.switchPinSize + token.switchPadding * 2}px + ${
token.switchInnerMarginMax * 2
}px)`,
}, },
[`${switchInnerCls}-unchecked`]: { [`${switchInnerCls}-unchecked`]: {
marginTop: -token.switchHeight, marginTop: calc(trackHeight).mul(-1).equal(),
marginInlineStart: 0, marginInlineStart: 0,
marginInlineEnd: 0, marginInlineEnd: 0,
}, },
}, },
[`&${componentCls}-checked ${switchInnerCls}`]: { [`&${componentCls}-checked ${switchInnerCls}`]: {
paddingInlineStart: token.switchInnerMarginMin, paddingInlineStart: innerMinMargin,
paddingInlineEnd: token.switchInnerMarginMax, paddingInlineEnd: innerMaxMargin,
[`${switchInnerCls}-checked`]: { [`${switchInnerCls}-checked`]: {
marginInlineStart: 0, marginInlineStart: 0,
marginInlineEnd: 0, marginInlineEnd: 0,
}, },
[`${switchInnerCls}-unchecked`]: { [`${switchInnerCls}-unchecked`]: {
marginInlineStart: `calc(100% - ${token.switchPinSize + token.switchPadding * 2}px + ${ marginInlineStart: `calc(100% - ${trackPaddingCalc} + ${innerMaxMarginCalc})`,
token.switchInnerMarginMax * 2 marginInlineEnd: `calc(-100% + ${trackPaddingCalc} - ${innerMaxMarginCalc})`,
}px)`,
marginInlineEnd: `calc(-100% + ${token.switchPinSize + token.switchPadding * 2}px - ${
token.switchInnerMarginMax * 2
}px)`,
}, },
}, },
[`&:not(${componentCls}-disabled):active`]: { [`&:not(${componentCls}-disabled):active`]: {
[`&:not(${componentCls}-checked) ${switchInnerCls}`]: { [`&:not(${componentCls}-checked) ${switchInnerCls}`]: {
[`${switchInnerCls}-unchecked`]: { [`${switchInnerCls}-unchecked`]: {
marginInlineStart: token.switchPadding * 2, marginInlineStart: calc(trackPadding).mul(2).equal(),
marginInlineEnd: -token.switchPadding * 2, marginInlineEnd: calc(trackPadding).mul(-1).mul(2).equal(),
}, },
}, },
[`&${componentCls}-checked ${switchInnerCls}`]: { [`&${componentCls}-checked ${switchInnerCls}`]: {
[`${switchInnerCls}-checked`]: { [`${switchInnerCls}-checked`]: {
marginInlineStart: -token.switchPadding * 2, marginInlineStart: calc(trackPadding).mul(-1).mul(2).equal(),
marginInlineEnd: token.switchPadding * 2, marginInlineEnd: calc(trackPadding).mul(2).equal(),
}, },
}, },
}, },
@ -253,7 +322,7 @@ const genSwitchInnerStyle: GenerateStyle<SwitchToken, CSSObject> = token => {
}; };
const genSwitchStyle = (token: SwitchToken): CSSObject => { const genSwitchStyle = (token: SwitchToken): CSSObject => {
const { componentCls } = token; const { componentCls, trackHeight, trackMinWidth } = token;
return { return {
[componentCls]: { [componentCls]: {
@ -262,9 +331,9 @@ const genSwitchStyle = (token: SwitchToken): CSSObject => {
position: 'relative', position: 'relative',
display: 'inline-block', display: 'inline-block',
boxSizing: 'border-box', boxSizing: 'border-box',
minWidth: token.switchMinWidth, minWidth: trackMinWidth,
height: token.switchHeight, height: trackHeight,
lineHeight: `${token.switchHeight}px`, lineHeight: unit(trackHeight),
verticalAlign: 'middle', verticalAlign: 'middle',
background: token.colorTextQuaternary, background: token.colorTextQuaternary,
border: '0', border: '0',
@ -306,48 +375,59 @@ const genSwitchStyle = (token: SwitchToken): CSSObject => {
}; };
// ============================== Export ============================== // ============================== Export ==============================
export default genComponentStyleHook('Switch', token => { export const prepareComponentToken: GetDefaultToken<'Switch'> = token => {
const switchHeight = token.fontSize * token.lineHeight; const { fontSize, lineHeight, controlHeight, colorWhite } = token;
const switchHeightSM = token.controlHeight / 2;
const switchPadding = 2; // This is magic
const switchPinSize = switchHeight - switchPadding * 2;
const switchPinSizeSM = switchHeightSM - switchPadding * 2;
const switchToken = mergeToken<SwitchToken>(token, { const height = fontSize * lineHeight;
switchMinWidth: switchPinSize * 2 + switchPadding * 4, const heightSM = controlHeight / 2;
switchHeight, const padding = 2; // Fixed value
switchDuration: token.motionDurationMid, const handleSize = height - padding * 2;
switchColor: token.colorPrimary, const handleSizeSM = heightSM - padding * 2;
switchDisabledOpacity: token.opacityLoading,
switchInnerMarginMin: switchPinSize / 2,
switchInnerMarginMax: switchPinSize + switchPadding + switchPadding * 2,
switchPadding,
switchPinSize,
switchBg: token.colorBgContainer,
switchMinWidthSM: switchPinSizeSM * 2 + switchPadding * 2,
switchHeightSM,
switchInnerMarginMinSM: switchPinSizeSM / 2,
switchInnerMarginMaxSM: switchPinSizeSM + switchPadding + switchPadding * 2,
switchPinSizeSM,
switchHandleShadow: `0 2px 4px 0 ${new TinyColor('#00230b').setAlpha(0.2).toRgbString()}`,
switchLoadingIconSize: token.fontSizeIcon * 0.75,
switchLoadingIconColor: `rgba(0, 0, 0, ${token.opacityLoading})`,
switchHandleActiveInset: '-30%',
});
return [ return {
genSwitchStyle(switchToken), trackHeight: height,
trackHeightSM: heightSM,
trackMinWidth: handleSize * 2 + padding * 4,
trackMinWidthSM: handleSizeSM * 2 + padding * 2,
trackPadding: padding, // Fixed value
handleBg: colorWhite,
handleSize,
handleSizeSM,
handleShadow: `0 2px 4px 0 ${new TinyColor('#00230b').setAlpha(0.2).toRgbString()}`,
innerMinMargin: handleSize / 2,
innerMaxMargin: handleSize + padding + padding * 2,
innerMinMarginSM: handleSizeSM / 2,
innerMaxMarginSM: handleSizeSM + padding + padding * 2,
};
};
// inner style export default genStyleHooks(
genSwitchInnerStyle(switchToken), 'Switch',
token => {
const switchToken = mergeToken<SwitchToken>(token, {
switchDuration: token.motionDurationMid,
switchColor: token.colorPrimary,
switchDisabledOpacity: token.opacityLoading,
switchLoadingIconSize: token.calc(token.fontSizeIcon).mul(0.75).equal(),
switchLoadingIconColor: `rgba(0, 0, 0, ${token.opacityLoading})`,
switchHandleActiveInset: '-30%',
});
// handle style return [
genSwitchHandleStyle(switchToken), genSwitchStyle(switchToken),
// loading style // inner style
genSwitchLoadingStyle(switchToken), genSwitchInnerStyle(switchToken),
// small style // handle style
genSwitchSmallStyle(switchToken), genSwitchHandleStyle(switchToken),
];
}); // loading style
genSwitchLoadingStyle(switchToken),
// small style
genSwitchSmallStyle(switchToken),
];
},
prepareComponentToken,
);

View File

@ -1,9 +0,0 @@
import getAlphaColor from '../util/getAlphaColor';
describe('util', () => {
describe('getAlphaColor', () => {
it('should not process color with alpha', () => {
expect(getAlphaColor('rgba(0, 0, 0, 0.5)', 'rgba(255, 255, 255)')).toBe('rgba(0, 0, 0, 0.5)');
});
});
});

Some files were not shown because too many files have changed in this diff Show More