Przeglądaj źródła

feat(组件): 更新同步

gemercheung 2 lat temu
rodzic
commit
6151bebec7
100 zmienionych plików z 4243 dodań i 660 usunięć
  1. 0 1
      .changeset/pre.json
  2. 1 0
      .eslintignore
  3. 1 1
      .github/workflows/tests.yml
  4. 56 47
      docs/.vitepress/config.mts
  5. 1 1
      docs/examples/dialog/focus-trapping.vue
  6. 1 1
      docs/examples/message-box/alert.vue
  7. 2 16
      docs/examples/minmap/basic.vue
  8. 178 0
      docs/zh-CN/component/message-box copy.md
  9. 6 172
      docs/zh-CN/component/message-box.md
  10. 1 1
      internal/build/src/build-info.ts
  11. 17 21
      package.json
  12. 1 1
      packages/components/CHANGELOG.md
  13. 12 14
      packages/components/README.md
  14. 1 2
      packages/components/advance/tag/src/tag.vue
  15. 0 25
      packages/components/basic/audio/__tests__/audio.test.tsx
  16. 9 0
      packages/components/basic/badge/index.ts
  17. 36 0
      packages/components/basic/badge/src/badge.ts
  18. 46 0
      packages/components/basic/badge/src/badge.vue
  19. 3 0
      packages/components/basic/badge/src/instance.ts
  20. 2 0
      packages/components/basic/badge/style/css.ts
  21. 2 0
      packages/components/basic/badge/style/index.ts
  22. 1 0
      packages/components/basic/button/index.ts
  23. 3 2
      packages/components/basic/button/src/button-custom.ts
  24. 2 2
      packages/components/basic/button/src/button-group.vue
  25. 12 7
      packages/components/basic/button/src/button.ts
  26. 5 7
      packages/components/basic/button/src/button.vue
  27. 1 1
      packages/tokens/button.ts
  28. 37 12
      packages/components/basic/button/src/use-button.ts
  29. 5 2
      packages/components/basic/config-provider/index.ts
  30. 68 0
      packages/components/basic/config-provider/src/config-provider-props.ts
  31. 5 50
      packages/components/basic/config-provider/src/config-provider.ts
  32. 1 1
      packages/tokens/config-provider.ts
  33. 57 5
      packages/hooks/use-global-config/index.ts
  34. 2 2
      packages/components/basic/config-provider/style/css.ts
  35. 2 2
      packages/components/basic/config-provider/style/index.ts
  36. 1 0
      packages/components/basic/dialog/index.ts
  37. 2 2
      packages/tokens/dialog.ts
  38. 31 19
      packages/components/basic/dialog/src/dialog-content.ts
  39. 19 21
      packages/components/basic/dialog/src/dialog-content.vue
  40. 45 12
      packages/components/basic/dialog/src/dialog.ts
  41. 38 9
      packages/components/basic/dialog/src/dialog.vue
  42. 3 3
      packages/components/basic/dialog/src/use-dialog.ts
  43. 1 1
      packages/components/basic/dialog/style/css.ts
  44. 2 0
      packages/components/basic/form-item/style/css.ts
  45. 2 0
      packages/components/basic/form-item/style/index.ts
  46. 18 0
      packages/components/basic/form/index.ts
  47. 122 0
      packages/components/basic/form/mocks/mock-data.tsx
  48. 7 0
      packages/components/basic/form/src/constants.ts
  49. 86 0
      packages/components/basic/form/src/form-item.ts
  50. 428 0
      packages/components/basic/form/src/form-item.vue
  51. 117 0
      packages/components/basic/form/src/form-label-wrap.tsx
  52. 120 0
      packages/components/basic/form/src/form.ts
  53. 200 0
      packages/components/basic/form/src/form.vue
  54. 2 0
      packages/components/basic/form/src/hooks/index.ts
  55. 44 0
      packages/components/basic/form/src/hooks/use-form-common-props.ts
  56. 92 0
      packages/components/basic/form/src/hooks/use-form-item.ts
  57. 141 0
      packages/components/basic/form/src/types.ts
  58. 57 0
      packages/components/basic/form/src/utils.ts
  59. 2 0
      packages/components/basic/form/style/css.ts
  60. 2 0
      packages/components/basic/form/style/index.ts
  61. 2 0
      packages/components/basic/index.ts
  62. 1 2
      packages/components/basic/input/index.ts
  63. 2 2
      packages/components/basic/input/src/input.ts
  64. 83 55
      packages/components/basic/input/src/input.vue
  65. 3 0
      packages/components/basic/input/src/instance.ts
  66. 2 2
      packages/components/basic/input/src/utils.ts
  67. 33 36
      packages/components/basic/message-box/src/index.vue
  68. 13 13
      packages/components/basic/message-box/src/message-box.type.ts
  69. 17 17
      packages/components/basic/message-box/src/messageBox.ts
  70. 7 0
      packages/components/basic/message/index.ts
  71. 35 0
      packages/components/basic/message/src/instance.ts
  72. 192 0
      packages/components/basic/message/src/message.ts
  73. 145 0
      packages/components/basic/message/src/message.vue
  74. 185 0
      packages/components/basic/message/src/method.ts
  75. 3 0
      packages/components/basic/message/style/css.ts
  76. 3 0
      packages/components/basic/message/style/index.ts
  77. 1 1
      packages/constants/date.ts
  78. 1 1
      packages/constants/size.ts
  79. 5 2
      packages/hooks/index.ts
  80. 0 46
      packages/hooks/use-common-props/index.ts
  81. 59 0
      packages/hooks/use-focus-controller/index.ts
  82. 13 0
      packages/hooks/use-focus/index.ts
  83. 6 8
      packages/hooks/use-id/index.ts
  84. 50 0
      packages/hooks/use-locale/index.ts
  85. 26 4
      packages/hooks/use-namespace/index.ts
  86. 30 0
      packages/hooks/use-size/index.ts
  87. 20 5
      packages/hooks/use-z-index/index.ts
  88. 0 1
      packages/kankan-components/index.ts
  89. 1 0
      packages/kankan-components/locales.ts
  90. 2 2
      packages/kankan-components/make-installer.ts
  91. 64 0
      packages/locale/index.ts
  92. 127 0
      packages/locale/lang/af.ts
  93. 147 0
      packages/locale/lang/ar.ts
  94. 130 0
      packages/locale/lang/az.ts
  95. 127 0
      packages/locale/lang/bg.ts
  96. 129 0
      packages/locale/lang/bn.ts
  97. 126 0
      packages/locale/lang/ca.ts
  98. 165 0
      packages/locale/lang/ckb.ts
  99. 129 0
      packages/locale/lang/cs.ts
  100. 0 0
      packages/locale/lang/da.ts

+ 0 - 1
.changeset/pre.json

@@ -7,7 +7,6 @@
     "@kankan-components/hooks": "0.0.1",
     "kankan-components": "0.0.1",
     "@kankan-components/theme-chalk": "0.1.0",
-    "@kankan-components/tokens": "0.0.1",
     "@kankan-components/utils": "1.0.0"
   },
   "changesets": []

+ 1 - 0
.eslintignore

@@ -2,4 +2,5 @@ node_modules
 !.*
 dist
 iconfont
+docs
 playground

+ 1 - 1
.github/workflows/tests.yml

@@ -3,7 +3,7 @@ name: tests
 on:
   push:
   pull_request:
-    branches: [ $default-branch ]
+    branches: [$default-branch]
 
 jobs:
   tests:

+ 56 - 47
docs/.vitepress/config.mts

@@ -6,28 +6,37 @@ import { features, head, mdPlugin, nav, sidebars } from './config'
 import type { UserConfig } from 'vitepress'
 
 const buildTransformers = () => {
-    const transformer = () => {
-        return {
-            props: [],
-            needRuntime: true,
-        }
+  const transformer = () => {
+    return {
+      props: [],
+      needRuntime: true,
     }
+  }
 
-    const transformers = {}
-    const directives = ['infinite-scroll', 'loading', 'popover', 'click-outside', 'repeat-click', 'trap-focus', 'mousewheel', 'resize']
-    directives.forEach(k => {
-        transformers[k] = transformer
-    })
+  const transformers = {}
+  const directives = [
+    'infinite-scroll',
+    'loading',
+    'popover',
+    'click-outside',
+    'repeat-click',
+    'trap-focus',
+    'mousewheel',
+    'resize',
+  ]
+  directives.forEach((k) => {
+    transformers[k] = transformer
+  })
 
-    return transformers
+  return transformers
 }
 
 consola.debug(`DOC_ENV: ${process.env.DOC_ENV}`)
 
 const languages = ['zh-CN']
 const locales = {
-    // '/en-US': { label: 'en-US', lang: 'en-US' },
-    '/zh-CN': { label: 'zh-CN', lang: 'zh-CN' },
+  // '/en-US': { label: 'en-US', lang: 'en-US' },
+  '/zh-CN': { label: 'zh-CN', lang: 'zh-CN' },
 }
 
 // languages.forEach(lang => {
@@ -41,46 +50,46 @@ const locales = {
 // consola.log('sidebars', sidebars)
 
 export const config: UserConfig = {
-    title: '看看公共组件',
-    description: '看看组件公共文档中心',
-    lastUpdated: true,
-    // base: "/kk-docs/",
-    head,
-    themeConfig: {
-        repo: 'http://192.168.0.115:3000/4dkankan/4dkankan-components',
-        docsBranch: REPO_BRANCH,
-        docsDir: docsDirName,
+  title: '看看公共组件',
+  description: '看看组件公共文档中心',
+  lastUpdated: true,
+  // base: "/kk-docs/",
+  head,
+  themeConfig: {
+    repo: 'http://192.168.0.115:3000/4dkankan/4dkankan-components',
+    docsBranch: REPO_BRANCH,
+    docsDir: docsDirName,
 
-        editLinks: true,
-        editLinkText: 'Edit this page on GitHub',
-        lastUpdated: 'Last Updated',
+    editLinks: true,
+    editLinkText: 'Edit this page on GitHub',
+    lastUpdated: 'Last Updated',
 
-        logo: '/images/logo.png',
-        logoSmall: '/images/kankan_icon.ico',
-        sidebars,
-        nav,
-        // agolia: {
-        //     apiKey: '377f2b647a96d9b1d62e4780f2344da2',
-        //     appId: 'BH4D9OD16A',
-        // },
-        features,
-        langs: languages,
-    },
+    logo: '/images/logo.png',
+    logoSmall: '/images/kankan_icon.ico',
+    sidebars,
+    nav,
+    // agolia: {
+    //     apiKey: '377f2b647a96d9b1d62e4780f2344da2',
+    //     appId: 'BH4D9OD16A',
+    // },
+    features,
+    langs: languages,
+  },
 
-    locales,
+  locales,
 
-    markdown: {
-        config: md => mdPlugin(md),
-    },
+  markdown: {
+    config: (md) => mdPlugin(md),
+  },
 
-    vue: {
-        template: {
-            ssr: true,
-            compilerOptions: {
-                directiveTransforms: buildTransformers(),
-            },
-        },
+  vue: {
+    template: {
+      ssr: true,
+      compilerOptions: {
+        directiveTransforms: buildTransformers(),
+      },
     },
+  },
 }
 
 export default config

+ 1 - 1
docs/examples/dialog/focus-trapping.vue

@@ -32,7 +32,7 @@
 <script lang="ts" setup>
 import { ref } from 'vue'
 import { KkButton, KkDialog } from 'kankan-components'
-import type { ElInput } from 'element-plus'
+import { ElInput } from 'element-plus'
 
 const dialogVisible = ref(false)
 const inputRef = ref<InstanceType<typeof ElInput>>()

+ 1 - 1
docs/examples/message-box/alert.vue

@@ -3,7 +3,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ElMessage, KkButton, KkMessageBox } from 'kankan-components'
+import { KkButton, KkMessageBox } from 'kankan-components'
 import type { Action } from 'element-plus'
 
 const open = () => {

+ 2 - 16
docs/examples/minmap/basic.vue

@@ -32,22 +32,8 @@ onUnmounted(() => {
 <template>
   <div id="scene" class="scene">
     <div class="test-control">
-      <kk-button
-        @click="
-          () => {
-            mapShow = true
-          }
-        "
-        >打开小地图</kk-button
-      >
-      <kk-button
-        type="primary"
-        mr4
-        @click="
-          () => {
-            mapShow = false
-          }
-        "
+      <kk-button @click="mapShow = true">打开小地图</kk-button>
+      <kk-button type="primary" mr4 @click="mapShow = false"
         >关闭小地图</kk-button
       >
     </div>

Plik diff jest za duży
+ 178 - 0
docs/zh-CN/component/message-box copy.md


Plik diff jest za duży
+ 6 - 172
docs/zh-CN/component/message-box.md


+ 1 - 1
internal/build/src/build-info.ts

@@ -5,7 +5,7 @@ import { epOutput } from '@kankan-components/build-utils'
 import type { ModuleFormat } from 'rollup'
 
 export const modules = ['esm', 'cjs'] as const
-export type Module = (typeof modules)[number]
+export type Module = typeof modules[number]
 export interface BuildInfo {
   module: 'ESNext' | 'CommonJS'
   format: ModuleFormat

+ 17 - 21
package.json

@@ -12,6 +12,7 @@
     "test:coverage": "vitest --coverage",
     "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx,.md,.json --max-warnings 0 --cache",
     "lint:fix": "pnpm run lint --fix",
+    "format": "prettier --write --cache .",
     "commit": "git cz",
     "preinstall": "npx only-allow pnpm",
     "changeset": "changeset",
@@ -39,17 +40,16 @@
     "@kankan-components/hooks": "workspace:*",
     "@kankan-components/icons-vue": "^0.0.1",
     "@kankan-components/theme-chalk": "workspace:*",
-    "@kankan-components/tokens": "workspace:*",
     "@kankan-components/utils": "workspace:*",
-    "@vueuse/core": "^9.12.0",
+    "@vueuse/core": "^9.13.0",
     "lodash": "^4.17.21",
     "lodash-es": "^4.17.21",
     "lodash-unified": "^1.0.3",
-    "vue": "^3.2.47"
+    "vue": "^3.3.4"
   },
   "devDependencies": {
-    "@changesets/cli": "^2.26.0",
-    "@commitlint/cli": "^17.4.2",
+    "@changesets/cli": "^2.26.2",
+    "@commitlint/cli": "^17.7.1",
     "@kankan-components/build": "workspace:*",
     "@kankan-components/build-utils": "workspace:*",
     "@kankan-components/eslint-config": "workspace:*",
@@ -57,32 +57,28 @@
     "@pnpm/logger": "^4.0.0",
     "@pnpm/types": "^8.10.0",
     "@types/fs-extra": "^9.0.13",
-    "@types/gulp": "^4.0.10",
+    "@types/gulp": "^4.0.13",
     "@types/jsdom": "^16.2.15",
-    "@types/node": "^18.13.0",
-    "@types/sass": "^1.43.1",
-    "@typescript-eslint/eslint-plugin": "^5.51.0",
+    "@types/node": "^18.17.5",
+    "@types/sass": "^1.45.0",
     "@vitejs/plugin-vue": "^3.2.0",
     "@vitejs/plugin-vue-jsx": "^2.1.1",
-    "@vue/test-utils": "^2.2.10",
+    "@vue/test-utils": "^2.4.1",
     "commitizen": "^4.3.0",
     "commitlint-config-cz": "^0.13.3",
     "concurrently": "^7.6.0",
     "cz-customizable": "^7.0.0",
-    "eslint": "~8.23.1",
-    "eslint-config-prettier": "^8.6.0",
-    "eslint-define-config": "^1.15.0",
-    "eslint-plugin-import": "~2.26.0",
-    "eslint-plugin-jest": "^25.7.0",
-    "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-vue": "^8.7.1",
+    "eslint": "^8.18.0",
+    "eslint-define-config": "^1.5.1",
     "husky": "^8.0.3",
-    "jest": "^29.4.2",
+    "jest": "^29.6.2",
     "jsdom": "16.4.0",
-    "lint-staged": "^13.1.1",
+    "lint-staged": "^13.3.0",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.7.1",
     "resize-observer-polyfill": "^1.5.1",
-    "sass": "^1.58.0",
-    "typescript": "~4.7.4",
+    "sass": "^1.66.0",
+    "typescript": "^4.9.5",
     "unplugin-vue-components": "^0.20.1",
     "unplugin-vue-macros": "^0.11.2",
     "vitest": "^0.23.4"

+ 1 - 1
packages/components/CHANGELOG.md

@@ -4,4 +4,4 @@
 
 ### Major Changes
 
--   初始发布
+- 初始发布

+ 12 - 14
packages/components/README.md

@@ -1,14 +1,12 @@
-# Components 组件库说明
-
-packages/components 组件目录结构
-```
-├── basic // 基础 UI 组件
-├── editor // 与编辑器相关
-├── advance // 高阶复杂
-
-```
-
-## 文件开发目录规范
-
-
-
+# Components 组件库说明
+
+packages/components 组件目录结构
+
+```
+├── basic // 基础 UI 组件
+├── editor // 与编辑器相关
+├── advance // 高阶复杂
+
+```
+
+## 文件开发目录规范

+ 1 - 2
packages/components/advance/tag/src/tag.vue

@@ -1,12 +1,11 @@
 <script setup lang="ts">
-import { inject, ref, unref, useSlots } from 'vue'
+import { ref, useSlots } from 'vue'
 import ShowTag from './showTag.vue'
 import { tagProps } from './tag'
 
 defineOptions({
   name: 'KkTag',
 })
-const sdk = inject('__sdk')
 
 const props = defineProps(tagProps)
 const slots = useSlots()

+ 0 - 25
packages/components/basic/audio/__tests__/audio.test.tsx

@@ -1,25 +0,0 @@
-import { ref } from 'vue'
-import { mount } from '@vue/test-utils'
-import { describe, expect, test } from 'vitest'
-import Audio from '../src/audio.vue'
-// import type { VNode } from 'vue';
-
-// const _mount = (render: () => VNode) => {
-//     return mount(render, { attachTo: document.body })
-//   }
-
-describe('Audio.vue', () => {
-  //   test('render test', async () => {
-  //     const AudioSrc = '';
-  //     const wrapper = mount(() => <Audio src={AudioSrc}></Audio>);
-  //     await nextTick();
-  //   });
-
-  test('play', async () => {
-    const radio = ref('')
-    const wrapper = mount(() => <Audio v-model={radio.value} />)
-    await wrapper.trigger('click')
-    expect(radio.value).toBe('')
-    expect(wrapper.classes()).toContain('is-disabled')
-  })
-})

+ 9 - 0
packages/components/basic/badge/index.ts

@@ -0,0 +1,9 @@
+import { withInstall } from '@kankan-components/utils'
+
+import Badge from './src/badge.vue'
+
+export const ElBadge = withInstall(Badge)
+export default ElBadge
+
+export * from './src/badge'
+export type { BadgeInstance } from './src/instance'

+ 36 - 0
packages/components/basic/badge/src/badge.ts

@@ -0,0 +1,36 @@
+import { buildProps } from '@kankan-components/utils'
+import type { ExtractPropTypes } from 'vue'
+
+export const badgeProps = buildProps({
+  /**
+   * @description display value.
+   */
+  value: {
+    type: [String, Number],
+    default: '',
+  },
+  /**
+   * @description maximum value, shows `{max}+` when exceeded. Only works if value is a number.
+   */
+  max: {
+    type: Number,
+    default: 99,
+  },
+  /**
+   * @description if a little dot is displayed.
+   */
+  isDot: Boolean,
+  /**
+   * @description hidden badge.
+   */
+  hidden: Boolean,
+  /**
+   * @description badge type.
+   */
+  type: {
+    type: String,
+    values: ['primary', 'success', 'warning', 'info', 'danger'],
+    default: 'danger',
+  },
+} as const)
+export type BadgeProps = ExtractPropTypes<typeof badgeProps>

+ 46 - 0
packages/components/basic/badge/src/badge.vue

@@ -0,0 +1,46 @@
+<template>
+  <div :class="ns.b()">
+    <slot />
+    <transition :name="`${ns.namespace.value}-zoom-in-center`">
+      <sup
+        v-show="!hidden && (content || isDot)"
+        :class="[
+          ns.e('content'),
+          ns.em('content', type),
+          ns.is('fixed', !!$slots.default),
+          ns.is('dot', isDot),
+        ]"
+        v-text="content"
+      />
+    </transition>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import { useNamespace } from '@kankan-components/hooks'
+import { isNumber } from '@kankan-components/utils'
+import { badgeProps } from './badge'
+
+defineOptions({
+  name: 'KkBadge',
+})
+
+const props = defineProps(badgeProps)
+
+const ns = useNamespace('badge')
+
+const content = computed<string>(() => {
+  if (props.isDot) return ''
+
+  if (isNumber(props.value) && isNumber(props.max)) {
+    return props.max < props.value ? `${props.max}+` : `${props.value}`
+  }
+  return `${props.value}`
+})
+
+defineExpose({
+  /** @description badge content */
+  content,
+})
+</script>

+ 3 - 0
packages/components/basic/badge/src/instance.ts

@@ -0,0 +1,3 @@
+import type Badge from './badge.vue'
+
+export type BadgeInstance = InstanceType<typeof Badge>

+ 2 - 0
packages/components/basic/badge/style/css.ts

@@ -0,0 +1,2 @@
+import '@element-plus/components/base/style/css'
+import '@element-plus/theme-chalk/el-badge.css'

+ 2 - 0
packages/components/basic/badge/style/index.ts

@@ -0,0 +1,2 @@
+import '@element-plus/components/base/style'
+import '@element-plus/theme-chalk/src/badge.scss'

+ 1 - 0
packages/components/basic/button/index.ts

@@ -9,4 +9,5 @@ export const KkButtonGroup = withNoopInstall(ButtonGroup)
 export default KkButton
 
 export * from './src/button'
+export * from './src/constants'
 export type { ButtonInstance, ButtonGroupInstance } from './src/instance'

+ 3 - 2
packages/components/basic/button/src/button-custom.ts

@@ -1,6 +1,7 @@
 import { computed } from 'vue'
 import { TinyColor } from '@ctrl/tinycolor'
-import { useDisabled, useNamespace } from '@kankan-components/hooks'
+import { useNamespace } from '@kankan-components/hooks'
+import { useFormDisabled } from '@kankan-components/components/basic/form'
 import type { ButtonProps } from './button'
 
 export function darken(color: TinyColor, amount = 20) {
@@ -8,7 +9,7 @@ export function darken(color: TinyColor, amount = 20) {
 }
 
 export function useButtonCustomStyle(props: ButtonProps) {
-  const _disabled = useDisabled()
+  const _disabled = useFormDisabled()
   const ns = useNamespace('button')
 
   // calculate hover & active color by custom color

+ 2 - 2
packages/components/basic/button/src/button-group.vue

@@ -5,12 +5,12 @@
 </template>
 <script lang="ts" setup>
 import { provide, reactive, toRef } from 'vue'
-import { buttonGroupContextKey } from '@kankan-components/tokens'
 import { useNamespace } from '@kankan-components/hooks'
 import { buttonGroupProps } from './button-group'
+import { buttonGroupContextKey } from './constants'
 
 defineOptions({
-  name: 'KkButtonGroup',
+  name: 'ElButtonGroup',
 })
 const props = defineProps(buttonGroupProps)
 provide(

+ 12 - 7
packages/components/basic/button/src/button.ts

@@ -1,14 +1,12 @@
 import { useSizeProp } from '@kankan-components/hooks'
-import { buildProps, definePropType } from '@kankan-components/utils'
+import {
+  buildProps,
+  definePropType,
+  iconPropType,
+} from '@kankan-components/utils'
 import { Loading } from '@kankan-components/icons-vue'
 import type { Component, ExtractPropTypes } from 'vue'
 
-const iconPropType = definePropType<string | Component>([
-  String,
-  Object,
-  Function,
-])
-
 export const buttonTypes = [
   'default',
   'primary',
@@ -110,6 +108,13 @@ export const buttonProps = buildProps({
     type: Boolean,
     default: undefined,
   },
+  /**
+   * @description custom element tag
+   */
+  tag: {
+    type: definePropType<string | Component>([String, Object]),
+    default: 'button',
+  },
 } as const)
 export const buttonEmits = {
   click: (evt: MouseEvent) => evt instanceof MouseEvent,

+ 5 - 7
packages/components/basic/button/src/button.vue

@@ -1,6 +1,8 @@
 <template>
-  <button
+  <component
+    :is="tag"
     ref="_ref"
+    v-bind="_props"
     :class="[
       ns.b(),
       ns.m(_type),
@@ -14,10 +16,6 @@
       ns.is('link', link),
       ns.is('has-bg', bg),
     ]"
-    :aria-disabled="_disabled || loading"
-    :disabled="_disabled || loading"
-    :autofocus="autofocus"
-    :type="nativeType"
     :style="buttonStyle"
     @click="handleClick"
   >
@@ -37,7 +35,7 @@
     >
       <slot />
     </span>
-  </button>
+  </component>
 </template>
 
 <script lang="ts" setup>
@@ -56,7 +54,7 @@ const emit = defineEmits(buttonEmits)
 
 const buttonStyle = useButtonCustomStyle(props)
 const ns = useNamespace('button')
-const { _ref, _size, _type, _disabled, shouldAddSpace, handleClick } =
+const { _ref, _size, _type, _disabled, _props, shouldAddSpace, handleClick } =
   useButton(props, emit)
 
 defineExpose({

+ 1 - 1
packages/tokens/button.ts

@@ -1,6 +1,6 @@
 import type { InjectionKey } from 'vue'
 
-import type { ButtonProps } from '@kankan-components/components/basic/button'
+import type { ButtonProps } from './button'
 
 export interface ButtonGroupContext {
   size?: ButtonProps['size']

+ 37 - 12
packages/components/basic/button/src/use-button.ts

@@ -1,11 +1,12 @@
 import { Text, computed, inject, ref, useSlots } from 'vue'
 import {
-  useDisabled,
-  // useFormItem,
-  useGlobalConfig,
-  useSize,
-} from '@kankan-components/hooks'
-import { buttonGroupContextKey } from '@kankan-components/tokens'
+  useFormDisabled,
+  useFormItem,
+  useFormSize,
+} from '@kankan-components/components/basic/form'
+import { useGlobalConfig } from '@kankan-components/components/basic/config-provider'
+// import { useDeprecated } from '@kankan-components/hooks'
+import { buttonGroupContextKey } from './constants'
 
 import type { SetupContext } from 'vue'
 import type { ButtonEmits, ButtonProps } from './button'
@@ -14,11 +15,22 @@ export const useButton = (
   props: ButtonProps,
   emit: SetupContext<ButtonEmits>['emit']
 ) => {
+  // useDeprecated(
+  //   {
+  //     from: 'type.text',
+  //     replacement: 'link',
+  //     version: '3.0.0',
+  //     scope: 'props',
+  //     ref: 'https://element-plus.org/en-US/component/button.html#button-attributes',
+  //   },
+  //   computed(() => props.type === 'text')
+  // )
+
   const buttonGroupContext = inject(buttonGroupContextKey, undefined)
   const globalConfig = useGlobalConfig('button')
-  // const { form } = useFormItem()
-  const _size = useSize(computed(() => buttonGroupContext?.size))
-  const _disabled = useDisabled()
+  const { form } = useFormItem()
+  const _size = useFormSize(computed(() => buttonGroupContext?.size))
+  const _disabled = useFormDisabled()
   const _ref = ref<HTMLButtonElement>()
   const slots = useSlots()
 
@@ -27,6 +39,18 @@ export const useButton = (
     () => props.autoInsertSpace ?? globalConfig.value?.autoInsertSpace ?? false
   )
 
+  const _props = computed(() => {
+    if (props.tag === 'button') {
+      return {
+        ariaDisabled: _disabled.value || props.loading,
+        disabled: _disabled.value || props.loading,
+        autofocus: props.autofocus,
+        type: props.nativeType,
+      }
+    }
+    return {}
+  })
+
   // add space between two characters in Chinese
   const shouldAddSpace = computed(() => {
     const defaultSlot = slots.default?.()
@@ -41,9 +65,9 @@ export const useButton = (
   })
 
   const handleClick = (evt: MouseEvent) => {
-    // if (props.nativeType === 'reset') {
-    //   form?.resetFields()
-    // }
+    if (props.nativeType === 'reset') {
+      form?.resetFields()
+    }
     emit('click', evt)
   }
 
@@ -52,6 +76,7 @@ export const useButton = (
     _size,
     _type,
     _ref,
+    _props,
     shouldAddSpace,
     handleClick,
   }

+ 5 - 2
packages/components/basic/config-provider/index.ts

@@ -2,7 +2,10 @@ import { withInstall } from '@kankan-components/utils'
 
 import ConfigProvider from './src/config-provider'
 
-export const UIConfigProvider = withInstall(ConfigProvider)
-export default UIConfigProvider
+export const ElConfigProvider = withInstall(ConfigProvider)
+export default ElConfigProvider
 
 export * from './src/config-provider'
+export * from './src/config-provider-props'
+export * from './src/constants'
+export * from './src/hooks/use-global-config'

+ 68 - 0
packages/components/basic/config-provider/src/config-provider-props.ts

@@ -0,0 +1,68 @@
+import { buildProps, definePropType } from '@kankan-components/utils'
+import { useSizeProp } from '@kankan-components/hooks'
+
+import type { ExtractPropTypes } from 'vue'
+import type { Language } from '@kankan-components/locale'
+import type { ButtonConfigContext } from '@kankan-components/components/basic/button'
+import type { MessageConfigContext } from '@kankan-components/components/basic/message'
+
+export type ExperimentalFeatures = {
+  // TO BE Defined
+}
+
+export const configProviderProps = buildProps({
+  /**
+   * @description Controlling if the users want a11y features
+   */
+  a11y: {
+    type: Boolean,
+    default: true,
+  },
+  /**
+   * @description Locale Object
+   */
+  locale: {
+    type: definePropType<Language>(Object),
+  },
+  /**
+   * @description global component size
+   */
+  size: useSizeProp,
+  /**
+   * @description button related configuration, [see the following table](#button-attributes)
+   */
+  button: {
+    type: definePropType<ButtonConfigContext>(Object),
+  },
+  /**
+   * @description features at experimental stage to be added, all features are default to be set to false                                                                                | ^[object]
+   */
+  experimentalFeatures: {
+    type: definePropType<ExperimentalFeatures>(Object),
+  },
+  /**
+   * @description Controls if we should handle keyboard navigation
+   */
+  keyboardNavigation: {
+    type: Boolean,
+    default: true,
+  },
+  /**
+   * @description message related configuration, [see the following table](#message-attributes)
+   */
+  message: {
+    type: definePropType<MessageConfigContext>(Object),
+  },
+  /**
+   * @description global Initial zIndex
+   */
+  zIndex: Number,
+  /**
+   * @description global component className prefix (cooperated with [$namespace](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/mixins/config.scss#L1)) | ^[string]
+   */
+  namespace: {
+    type: String,
+    default: 'el',
+  },
+} as const)
+export type ConfigProviderProps = ExtractPropTypes<typeof configProviderProps>

+ 5 - 50
packages/components/basic/config-provider/src/config-provider.ts

@@ -1,58 +1,13 @@
 import { defineComponent, renderSlot, watch } from 'vue'
-import { buildProps, definePropType } from '@kankan-components/utils'
-import { provideGlobalConfig, useSizeProp } from '@kankan-components/hooks'
+import { provideGlobalConfig } from './hooks/use-global-config'
+import { configProviderProps } from './config-provider-props'
 
-import type { ExtractPropTypes } from 'vue'
-// import type { ExperimentalFeatures } from '@kankan-components/tokens'
-// import type { Language } from '@kankan-components/locale'
-import type { ButtonConfigContext } from '@kankan-components/components/basic/button'
-// import type { MessageConfigContext } from '@kankan-components/components/message'
+import type { MessageConfigContext } from '@kankan-components/components/basic/message'
 
-// export const messageConfig: MessageConfigContext = {}
-export const messageConfig = {}
-
-export const configProviderProps = buildProps({
-  // Controlling if the users want a11y features.
-  a11y: {
-    type: Boolean,
-    default: true,
-  },
-
-  // locale: {
-  //     type: definePropType<Language>(Object),
-  // },
-
-  size: useSizeProp,
-
-  button: {
-    type: definePropType<ButtonConfigContext>(Object),
-  },
-
-  // experimentalFeatures: {
-  //     type: definePropType<ExperimentalFeatures>(Object),
-  // },
-
-  // Controls if we should handle keyboard navigation
-  keyboardNavigation: {
-    type: Boolean,
-    default: true,
-  },
-
-  message: {
-    type: definePropType<any>(Object),
-  },
-
-  zIndex: Number,
-
-  namespace: {
-    type: String,
-    default: 'kk',
-  },
-} as const)
-export type ConfigProviderProps = ExtractPropTypes<typeof configProviderProps>
+export const messageConfig: MessageConfigContext = {}
 
 const ConfigProvider = defineComponent({
-  name: 'UIConfigProvider',
+  name: 'KkConfigProvider',
   props: configProviderProps,
 
   setup(props, { slots }) {

+ 1 - 1
packages/tokens/config-provider.ts

@@ -1,4 +1,4 @@
-import type { ConfigProviderProps } from '@kankan-components/components/basic/config-provider'
+import type { ConfigProviderProps } from './config-provider-props'
 import type { InjectionKey, Ref } from 'vue'
 
 export type ConfigProviderContext = Partial<ConfigProviderProps>

+ 57 - 5
packages/hooks/use-global-config/index.ts

@@ -1,14 +1,22 @@
 import { computed, getCurrentInstance, inject, provide, ref, unref } from 'vue'
-import { configProviderContextKey } from '@kankan-components/tokens'
 import { debugWarn, keysOf } from '@kankan-components/utils'
+import {
+  SIZE_INJECTION_KEY,
+  defaultInitialZIndex,
+  defaultNamespace,
+  localeContextKey,
+  namespaceContextKey,
+  useLocale,
+  useNamespace,
+  useZIndex,
+  zIndexContextKey,
+} from '@kankan-components/hooks'
+import { configProviderContextKey } from '../constants'
 
 import type { MaybeRef } from '@vueuse/core'
 import type { App, Ref } from 'vue'
-import type { ConfigProviderContext } from '@kankan-components/tokens'
+import type { ConfigProviderContext } from '../constants'
 
-// this is meant to fix global methods like `ElMessage(opts)`, this way we can inject current locale
-// into the component as default injection value.
-// refer to: https://github.com/element-plus/element-plus/issues/2610#issuecomment-887965266
 const globalConfig = ref<ConfigProviderContext>()
 
 export function useGlobalConfig<
@@ -33,6 +41,33 @@ export function useGlobalConfig(
   }
 }
 
+// for components like `ElMessage` `ElNotification` `ElMessageBox`.
+export function useGlobalComponentSettings(
+  block: string,
+  sizeFallback?: MaybeRef<ConfigProviderContext['size']>
+) {
+  const config = useGlobalConfig()
+
+  const ns = useNamespace(
+    block,
+    computed(() => config.value?.namespace || defaultNamespace)
+  )
+
+  const locale = useLocale(computed(() => config.value?.locale))
+  const zIndex = useZIndex(
+    computed(() => config.value?.zIndex || defaultInitialZIndex)
+  )
+  const size = computed(() => unref(sizeFallback) || config.value?.size || '')
+  provideGlobalConfig(computed(() => unref(config) || {}))
+
+  return {
+    ns,
+    locale,
+    zIndex,
+    size,
+  }
+}
+
 export const provideGlobalConfig = (
   config: MaybeRef<ConfigProviderContext>,
   app?: App,
@@ -56,6 +91,23 @@ export const provideGlobalConfig = (
     return mergeConfig(oldConfig.value, cfg)
   })
   provideFn(configProviderContextKey, context)
+  provideFn(
+    localeContextKey,
+    computed(() => context.value.locale)
+  )
+  provideFn(
+    namespaceContextKey,
+    computed(() => context.value.namespace)
+  )
+  provideFn(
+    zIndexContextKey,
+    computed(() => context.value.zIndex)
+  )
+
+  provideFn(SIZE_INJECTION_KEY, {
+    size: computed(() => context.value.size || ''),
+  })
+
   if (global || !globalConfig.value) {
     globalConfig.value = context.value
   }

+ 2 - 2
packages/components/basic/config-provider/style/css.ts

@@ -1,2 +1,2 @@
-// import '@element-plus/components/base/style/css'
-// import '@element-plus/theme-chalk/el-config-provider.css'
+import '@kankan-components/components/base/style/css'
+import '@kankan-components/theme-chalk/el-config-provider.css'

+ 2 - 2
packages/components/basic/config-provider/style/index.ts

@@ -1,2 +1,2 @@
-// import '@element-plus/components/base/style'
-// import '@element-plus/theme-chalk/src/config-provider.scss'
+import '@kankan-components/components/base/style'
+import '@kankan-components/theme-chalk/src/config-provider.scss'

+ 1 - 0
packages/components/basic/dialog/index.ts

@@ -6,3 +6,4 @@ export default KkDialog
 
 export * from './src/use-dialog'
 export * from './src/dialog'
+export * from './src/constants'

+ 2 - 2
packages/tokens/dialog.ts

@@ -1,11 +1,11 @@
 import type { CSSProperties, ComputedRef, InjectionKey, Ref } from 'vue'
-import type { useNamespace } from '@kankan-components/hooks'
+import type { UseNamespaceReturn } from '@kankan-components/hooks'
 
 export type DialogContext = {
   dialogRef: Ref<HTMLElement | undefined>
   headerRef: Ref<HTMLElement | undefined>
   bodyId: Ref<string>
-  ns: ReturnType<typeof useNamespace>
+  ns: UseNamespaceReturn
   rendered: Ref<boolean>
   style: ComputedRef<CSSProperties>
 }

+ 31 - 19
packages/components/basic/dialog/src/dialog-content.ts

@@ -1,33 +1,45 @@
-import { buildProps } from '@kankan-components/utils'
+import { buildProps, iconPropType } from '@kankan-components/utils'
 
 export const dialogContentProps = buildProps({
-  center: {
-    type: Boolean,
-    default: false,
-  },
-  alignCenter: {
-    type: Boolean,
-    default: false,
+  /**
+   * @description whether to align the header and footer in center
+   */
+  center: Boolean,
+  /**
+   * @description whether to align the dialog both horizontally and vertically
+   */
+  alignCenter: Boolean,
+  /**
+   * @description custom close icon, default is Close
+   */
+  closeIcon: {
+    type: iconPropType,
   },
-  // closeIcon: {
-  //   type: iconPropType,
-  // },
+  /**
+   * @deprecated will be removed in version 2.4.0, please use class
+   */
   customClass: {
     type: String,
     default: '',
   },
-  draggable: {
-    type: Boolean,
-    default: false,
-  },
-  fullscreen: {
-    type: Boolean,
-    default: false,
-  },
+  /**
+   * @description enable dragging feature for Dialog
+   */
+  draggable: Boolean,
+  /**
+   * @description whether the Dialog takes up full screen
+   */
+  fullscreen: Boolean,
+  /**
+   * @description whether to show a close button
+   */
   showClose: {
     type: Boolean,
     default: true,
   },
+  /**
+   * @description title of Dialog. Can also be passed with a named slot (see the following table)
+   */
   title: {
     type: String,
     default: '',

+ 19 - 21
packages/components/basic/dialog/src/dialog-content.vue

@@ -1,17 +1,5 @@
 <template>
-  <div
-    :ref="composedDialogRef"
-    :class="[
-      ns.b(),
-      ns.is('fullscreen', fullscreen),
-      ns.is('draggable', draggable),
-      ns.is('align-center', alignCenter),
-      { [ns.m('center')]: center },
-      customClass,
-    ]"
-    :style="style"
-    tabindex="-1"
-  >
+  <div :ref="composedDialogRef" :class="dialogKls" :style="style" tabindex="-1">
     <header ref="headerRef" :class="ns.e('header')">
       <slot name="header">
         <span role="heading" :class="ns.e('title')">
@@ -20,12 +8,13 @@
       </slot>
       <button
         v-if="showClose"
+        :aria-label="t('el.dialog.close')"
         :class="ns.e('headerbtn')"
         type="button"
         @click="$emit('close')"
       >
-        <kk-icon :class="ns.e('close')" type="close">
-          <!-- <component :is="closeIcon" /> -->
+        <kk-icon :class="ns.e('close')">
+          <component :is="closeIcon || Close" />
         </kk-icon>
       </button>
     </header>
@@ -42,21 +31,30 @@
 import { computed, inject } from 'vue'
 import { KkIcon } from '@kankan-components/components/basic/icon'
 import { FOCUS_TRAP_INJECTION_KEY } from '@kankan-components/components/basic/focus-trap'
-import { useDraggable } from '@kankan-components/hooks'
-import { composeRefs } from '@kankan-components/utils'
-import { dialogInjectionKey } from '@kankan-components/tokens'
+import { useDraggable, useLocale } from '@kankan-components/hooks'
+import { CloseComponents, composeRefs } from '@kankan-components/utils'
+import { dialogInjectionKey } from './constants'
 import { dialogContentEmits, dialogContentProps } from './dialog-content'
 
-// const { t } = useLocale()
-// const { Close } = CloseComponents
+const { t } = useLocale()
+const { Close } = CloseComponents
 
-defineOptions({ name: 'UiDialogContent' })
+defineOptions({ name: 'ElDialogContent' })
 const props = defineProps(dialogContentProps)
 defineEmits(dialogContentEmits)
 
 const { dialogRef, headerRef, bodyId, ns, style } = inject(dialogInjectionKey)!
 const { focusTrapRef } = inject(FOCUS_TRAP_INJECTION_KEY)!
 
+const dialogKls = computed(() => [
+  ns.b(),
+  ns.is('fullscreen', props.fullscreen),
+  ns.is('draggable', props.draggable),
+  ns.is('align-center', props.alignCenter),
+  { [ns.m('center')]: props.center },
+  props.customClass,
+])
+
 const composedDialogRef = composeRefs(focusTrapRef, dialogRef)
 
 const draggable = computed(() => props.draggable)

+ 45 - 12
packages/components/basic/dialog/src/dialog.ts

@@ -9,52 +9,85 @@ export type DialogBeforeCloseFn = (done: DoneFn) => void
 
 export const dialogProps = buildProps({
   ...dialogContentProps,
-  appendToBody: {
-    type: Boolean,
-    default: false,
-  },
+  /**
+   * @description whether to append Dialog itself to body. A nested Dialog should have this attribute set to `true`
+   */
+  appendToBody: Boolean,
+  /**
+   * @description callback before Dialog closes, and it will prevent Dialog from closing, use done to close the dialog
+   */
   beforeClose: {
     type: definePropType<DialogBeforeCloseFn>(Function),
   },
-  destroyOnClose: {
-    type: Boolean,
-    default: false,
-  },
+  /**
+   * @description destroy elements in Dialog when closed
+   */
+  destroyOnClose: Boolean,
+  /**
+   * @description whether the Dialog can be closed by clicking the mask
+   */
   closeOnClickModal: {
     type: Boolean,
     default: true,
   },
+  /**
+   * @description whether the Dialog can be closed by pressing ESC
+   */
   closeOnPressEscape: {
     type: Boolean,
     default: true,
   },
+  /**
+   * @description whether scroll of body is disabled while Dialog is displayed
+   */
   lockScroll: {
     type: Boolean,
     default: true,
   },
+  /**
+   * @description whether a mask is displayed
+   */
   modal: {
     type: Boolean,
     default: true,
   },
+  /**
+   * @description the Time(milliseconds) before open
+   */
   openDelay: {
     type: Number,
     default: 0,
   },
+  /**
+   * @description the Time(milliseconds) before close
+   */
   closeDelay: {
     type: Number,
     default: 0,
   },
+  /**
+   * @description value for `margin-top` of Dialog CSS, default is 15vh
+   */
   top: {
     type: String,
   },
-  modelValue: {
-    type: Boolean,
-    default: false,
-  },
+  /**
+   * @description visibility of Dialog
+   */
+  modelValue: Boolean,
+  /**
+   * @description custom class names for mask
+   */
   modalClass: String,
+  /**
+   * @description width of Dialog, default is 50%
+   */
   width: {
     type: [String, Number],
   },
+  /**
+   * @description same as z-index in native CSS, z-order of dialog
+   */
   zIndex: {
     type: Number,
   },

+ 38 - 9
packages/components/basic/dialog/src/dialog.vue

@@ -25,7 +25,7 @@
           @mousedown="overlayEvent.onMousedown"
           @mouseup="overlayEvent.onMouseup"
         >
-          <ui-focus-trap
+          <kk-focus-trap
             loop
             :trapped="visible"
             focus-start-el="container"
@@ -34,13 +34,14 @@
             @focusout-prevented="onFocusoutPrevented"
             @release-requested="onCloseRequested"
           >
-            <ui-dialog-content
+            <kk-dialog-content
               v-if="rendered"
               ref="dialogContentRef"
               v-bind="$attrs"
               :custom-class="customClass"
               :center="center"
               :align-center="alignCenter"
+              :close-icon="closeIcon"
               :draggable="draggable"
               :fullscreen="fullscreen"
               :show-close="showClose"
@@ -61,8 +62,8 @@
               <template v-if="$slots.footer" #footer>
                 <slot name="footer" />
               </template>
-            </ui-dialog-content>
-          </ui-focus-trap>
+            </kk-dialog-content>
+          </kk-focus-trap>
         </div>
       </kk-overlay>
     </transition>
@@ -72,20 +73,48 @@
 <script lang="ts" setup>
 import { computed, provide, ref } from 'vue'
 import { KkOverlay } from '@kankan-components/components/basic/overlay'
-import { useNamespace, useSameTarget } from '@kankan-components/hooks'
-import { dialogInjectionKey } from '@kankan-components/tokens'
-import UiFocusTrap from '@kankan-components/components/basic/focus-trap'
-import UiDialogContent from './dialog-content.vue'
+import {
+  // useDeprecated,
+  useNamespace,
+  useSameTarget,
+} from '@kankan-components/hooks'
+import KkFocusTrap from '@kankan-components/components/basic/focus-trap'
+import KkDialogContent from './dialog-content.vue'
+import { dialogInjectionKey } from './constants'
 import { dialogEmits, dialogProps } from './dialog'
 import { useDialog } from './use-dialog'
 
 defineOptions({
-  name: 'UiDialog',
+  name: 'KkDialog',
   inheritAttrs: false,
 })
 
 const props = defineProps(dialogProps)
 defineEmits(dialogEmits)
+// const slots = useSlots()
+
+// useDeprecated(
+//   {
+//     scope: 'el-dialog',
+//     from: 'the title slot',
+//     replacement: 'the header slot',
+//     version: '3.0.0',
+//     ref: 'https://element-plus.org/en-US/component/dialog.html#slots',
+//   },
+//   computed(() => !!slots.title)
+// )
+
+// useDeprecated(
+//   {
+//     scope: 'el-dialog',
+//     from: 'custom-class',
+//     replacement: 'class',
+//     version: '2.3.0',
+//     ref: 'https://element-plus.org/en-US/component/dialog.html#attributes',
+//     type: 'Attribute',
+//   },
+//   computed(() => !!props.customClass)
+// )
 
 const ns = useNamespace('dialog')
 const dialogRef = ref<HTMLElement>()

+ 3 - 3
packages/components/basic/dialog/src/use-dialog.ts

@@ -6,17 +6,17 @@ import {
   ref,
   watch,
 } from 'vue'
-import { isClient, useTimeoutFn } from '@vueuse/core'
+import { useTimeoutFn } from '@vueuse/core'
 
 import {
   defaultNamespace,
-  useGlobalConfig,
   useId,
   useLockscreen,
   useZIndex,
 } from '@kankan-components/hooks'
 import { UPDATE_MODEL_EVENT } from '@kankan-components/constants'
-import { addUnit } from '@kankan-components/utils'
+import { addUnit, isClient } from '@kankan-components/utils'
+import { useGlobalConfig } from '@kankan-components/components/basic/config-provider'
 
 import type { CSSProperties, Ref, SetupContext } from 'vue'
 import type { DialogEmits, DialogProps } from './dialog'

+ 1 - 1
packages/components/basic/dialog/style/css.ts

@@ -1,3 +1,3 @@
 import '@kankan-components/components/base/style/css'
-import '@kankan-components/theme-chalk/el-dialog.css'
+import '@kankan-components/theme-chalk/kk-dialog.css'
 import '@kankan-components/components/overlay/style/css'

+ 2 - 0
packages/components/basic/form-item/style/css.ts

@@ -0,0 +1,2 @@
+import '@kankan-components/components/base/style/css'
+import '@kankan-components/theme-chalk/el-form-item.css'

+ 2 - 0
packages/components/basic/form-item/style/index.ts

@@ -0,0 +1,2 @@
+import '@kankan-components/components/base/style'
+import '@kankan-components/theme-chalk/src/form-item.scss'

+ 18 - 0
packages/components/basic/form/index.ts

@@ -0,0 +1,18 @@
+import { withInstall, withNoopInstall } from '@kankan-components/utils'
+import Form from './src/form.vue'
+import FormItem from './src/form-item.vue'
+
+export const KkForm = withInstall(Form, {
+  FormItem,
+})
+export default KkForm
+export const KkFormItem = withNoopInstall(FormItem)
+
+export * from './src/form'
+export * from './src/form-item'
+export * from './src/types'
+export * from './src/constants'
+export * from './src/hooks'
+
+export type FormInstance = InstanceType<typeof Form>
+export type FormItemInstance = InstanceType<typeof FormItem>

+ 122 - 0
packages/components/basic/form/mocks/mock-data.tsx

@@ -0,0 +1,122 @@
+import { defineComponent, ref, toRef } from 'vue'
+import Input from '@kankan-components/components/input'
+import Button from '@kankan-components/components/button'
+import Form from '../src/form.vue'
+import FormItem from '../src/form-item.vue'
+
+interface DomainItem {
+  key: number
+  value: string
+}
+
+const DynamicDomainForm = defineComponent({
+  props: {
+    onSuccess: Function,
+    onError: Function,
+    onSubmit: Function,
+    model: Object,
+  },
+  setup(props, { slots }) {
+    const propsModel = toRef(props, 'model')
+    const model = ref({
+      domains: [
+        {
+          key: 1,
+          value: '',
+        },
+      ],
+    })
+
+    const formRef = ref<InstanceType<typeof Form>>()
+
+    const removeDomain = (item: DomainItem) => {
+      const index = model.value.domains.indexOf(item)
+      if (index !== -1) {
+        model.value.domains.splice(index, 1)
+      }
+    }
+
+    const addDomain = () => {
+      model.value.domains.push({
+        key: Date.now(),
+        value: '',
+      })
+    }
+
+    const submitForm = async () => {
+      if (!formRef.value) return
+      try {
+        const validate = props.onSubmit
+          ? formRef.value.validate(props.onSubmit as any)
+          : formRef.value.validate()
+
+        await validate
+        props.onSuccess?.()
+      } catch (e) {
+        props.onError?.(e)
+      }
+    }
+
+    return () => (
+      <Form
+        ref={formRef}
+        model={{ ...model.value, ...(propsModel.value || {}) }}
+      >
+        {model.value.domains.map((domain, index) => {
+          return (
+            <FormItem
+              class="domain-item"
+              key={domain.key}
+              label={`Domain${index}`}
+              prop={`domains.${index}.value`}
+              rules={{
+                required: true,
+                message: 'domain can not be null',
+                trigger: 'blur',
+              }}
+            >
+              <Input v-model={domain.value} />
+              <Button
+                class={`delete-domain ${index}`}
+                onClick={(e) => {
+                  e.preventDefault()
+                  removeDomain(domain)
+                }}
+              >
+                Delete
+              </Button>
+            </FormItem>
+          )
+        })}
+        {slots.default?.()}
+
+        <FormItem>
+          <Button class="submit" type="primary" onClick={submitForm}>
+            Submit
+          </Button>
+          <Button class="add-domain" onClick={addDomain}>
+            New domain
+          </Button>
+        </FormItem>
+      </Form>
+    )
+  },
+})
+
+export default DynamicDomainForm
+
+export const formatDomainError = (count: number) => {
+  return Array.from({ length: count }).reduce((prev: any, _, idx) => {
+    const key = `domains.${idx}.value`
+    return {
+      ...prev,
+      [key]: [
+        {
+          field: key,
+          fieldValue: '',
+          message: 'domain can not be null',
+        },
+      ],
+    }
+  }, {})
+}

+ 7 - 0
packages/components/basic/form/src/constants.ts

@@ -0,0 +1,7 @@
+import type { InjectionKey } from 'vue'
+import type { FormContext, FormItemContext } from './types'
+
+export const formContextKey: InjectionKey<FormContext> =
+  Symbol('formContextKey')
+export const formItemContextKey: InjectionKey<FormItemContext> =
+  Symbol('formItemContextKey')

+ 86 - 0
packages/components/basic/form/src/form-item.ts

@@ -0,0 +1,86 @@
+import { componentSizes } from '@kankan-components/constants'
+import { buildProps, definePropType } from '@kankan-components/utils'
+
+import type { ExtractPropTypes } from 'vue'
+import type { Arrayable } from '@kankan-components/utils'
+import type { FormItemRule } from './types'
+
+export const formItemValidateStates = [
+  '',
+  'error',
+  'validating',
+  'success',
+] as const
+export type FormItemValidateState = typeof formItemValidateStates[number]
+
+export type FormItemProp = Arrayable<string>
+
+export const formItemProps = buildProps({
+  /**
+   * @description Label text.
+   */
+  label: String,
+  /**
+   * @description Width of label, e.g. `'50px'`. `'auto'` is supported.
+   */
+  labelWidth: {
+    type: [String, Number],
+    default: '',
+  },
+  /**
+   * @description  A key of `model`. It could be an array of property paths (e.g `['a', 'b', '0']`). In the use of `validate` and `resetFields` method, the attribute is required.
+   */
+  prop: {
+    type: definePropType<FormItemProp>([String, Array]),
+  },
+  /**
+   * @description Whether the field is required or not, will be determined by validation rules if omitted.
+   */
+  required: {
+    type: Boolean,
+    default: undefined,
+  },
+  /**
+   * @description Validation rules of form, see the [following table](#formitemrule), more advanced usage at [async-validator](https://github.com/yiminghe/async-validator).
+   */
+  rules: {
+    type: definePropType<Arrayable<FormItemRule>>([Object, Array]),
+  },
+  /**
+   * @description Field error message, set its value and the field will validate error and show this message immediately.
+   */
+  error: String,
+  /**
+   * @description Validation state of formItem.
+   */
+  validateStatus: {
+    type: String,
+    values: formItemValidateStates,
+  },
+  /**
+   * @description Same as for in native label.
+   */
+  for: String,
+  /**
+   * @description Inline style validate message.
+   */
+  inlineMessage: {
+    type: [String, Boolean],
+    default: '',
+  },
+  /**
+   * @description Whether to show the error message.
+   */
+  showMessage: {
+    type: Boolean,
+    default: true,
+  },
+  /**
+   * @description Control the size of components in this form-item.
+   */
+  size: {
+    type: String,
+    values: componentSizes,
+  },
+} as const)
+export type FormItemProps = ExtractPropTypes<typeof formItemProps>

+ 428 - 0
packages/components/basic/form/src/form-item.vue

@@ -0,0 +1,428 @@
+<template>
+  <div
+    ref="formItemRef"
+    :class="formItemClasses"
+    :role="isGroup ? 'group' : undefined"
+    :aria-labelledby="isGroup ? labelId : undefined"
+  >
+    <form-label-wrap
+      :is-auto-width="labelStyle.width === 'auto'"
+      :update-all="formContext?.labelWidth === 'auto'"
+    >
+      <component
+        :is="labelFor ? 'label' : 'div'"
+        v-if="hasLabel"
+        :id="labelId"
+        :for="labelFor"
+        :class="ns.e('label')"
+        :style="labelStyle"
+      >
+        <slot name="label" :label="currentLabel">
+          {{ currentLabel }}
+        </slot>
+      </component>
+    </form-label-wrap>
+
+    <div :class="ns.e('content')" :style="contentStyle">
+      <slot />
+      <transition-group :name="`${ns.namespace.value}-zoom-in-top`">
+        <slot v-if="shouldShowError" name="error" :error="validateMessage">
+          <div :class="validateClasses">
+            {{ validateMessage }}
+          </div>
+        </slot>
+      </transition-group>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  computed,
+  inject,
+  nextTick,
+  onBeforeUnmount,
+  onMounted,
+  provide,
+  reactive,
+  ref,
+  toRefs,
+  useSlots,
+  watch,
+} from 'vue'
+import AsyncValidator from 'async-validator'
+import { clone } from 'lodash-unified'
+import { refDebounced } from '@vueuse/core'
+import {
+  addUnit,
+  ensureArray,
+  getProp,
+  isBoolean,
+  isFunction,
+  isString,
+} from '@kankan-components/utils'
+import { useId, useNamespace } from '@kankan-components/hooks'
+import { useFormSize } from './hooks'
+import { formItemProps } from './form-item'
+import FormLabelWrap from './form-label-wrap'
+import { formContextKey, formItemContextKey } from './constants'
+
+import type { CSSProperties } from 'vue'
+import type { RuleItem } from 'async-validator'
+import type { Arrayable } from '@kankan-components/utils'
+import type {
+  FormItemContext,
+  FormItemRule,
+  FormValidateFailure,
+} from './types'
+import type { FormItemValidateState } from './form-item'
+
+defineOptions({
+  name: 'ElFormItem',
+})
+const props = defineProps(formItemProps)
+const slots = useSlots()
+
+const formContext = inject(formContextKey, undefined)
+const parentFormItemContext = inject(formItemContextKey, undefined)
+
+const _size = useFormSize(undefined, { formItem: false })
+const ns = useNamespace('form-item')
+
+const labelId = useId().value
+const inputIds = ref<string[]>([])
+
+const validateState = ref<FormItemValidateState>('')
+const validateStateDebounced = refDebounced(validateState, 100)
+const validateMessage = ref('')
+const formItemRef = ref<HTMLDivElement>()
+// special inline value.
+let initialValue: any = undefined
+let isResettingField = false
+
+const labelStyle = computed<CSSProperties>(() => {
+  if (formContext?.labelPosition === 'top') {
+    return {}
+  }
+
+  const labelWidth = addUnit(props.labelWidth || formContext?.labelWidth || '')
+  if (labelWidth) return { width: labelWidth }
+  return {}
+})
+
+const contentStyle = computed<CSSProperties>(() => {
+  if (formContext?.labelPosition === 'top' || formContext?.inline) {
+    return {}
+  }
+  if (!props.label && !props.labelWidth && isNested) {
+    return {}
+  }
+  const labelWidth = addUnit(props.labelWidth || formContext?.labelWidth || '')
+  if (!props.label && !slots.label) {
+    return { marginLeft: labelWidth }
+  }
+  return {}
+})
+
+const formItemClasses = computed(() => [
+  ns.b(),
+  ns.m(_size.value),
+  ns.is('error', validateState.value === 'error'),
+  ns.is('validating', validateState.value === 'validating'),
+  ns.is('success', validateState.value === 'success'),
+  ns.is('required', isRequired.value || props.required),
+  ns.is('no-asterisk', formContext?.hideRequiredAsterisk),
+  formContext?.requireAsteriskPosition === 'right'
+    ? 'asterisk-right'
+    : 'asterisk-left',
+  { [ns.m('feedback')]: formContext?.statusIcon },
+])
+
+const _inlineMessage = computed(() =>
+  isBoolean(props.inlineMessage)
+    ? props.inlineMessage
+    : formContext?.inlineMessage || false
+)
+
+const validateClasses = computed(() => [
+  ns.e('error'),
+  { [ns.em('error', 'inline')]: _inlineMessage.value },
+])
+
+const propString = computed(() => {
+  if (!props.prop) return ''
+  return isString(props.prop) ? props.prop : props.prop.join('.')
+})
+
+const hasLabel = computed<boolean>(() => {
+  return !!(props.label || slots.label)
+})
+
+const labelFor = computed<string | undefined>(() => {
+  return props.for || inputIds.value.length === 1
+    ? inputIds.value[0]
+    : undefined
+})
+
+const isGroup = computed<boolean>(() => {
+  return !labelFor.value && hasLabel.value
+})
+
+const isNested = !!parentFormItemContext
+
+const fieldValue = computed(() => {
+  const model = formContext?.model
+  if (!model || !props.prop) {
+    return
+  }
+  return getProp(model, props.prop).value
+})
+
+const normalizedRules = computed(() => {
+  const { required } = props
+
+  const rules: FormItemRule[] = []
+
+  if (props.rules) {
+    rules.push(...ensureArray(props.rules))
+  }
+
+  const formRules = formContext?.rules
+  if (formRules && props.prop) {
+    const _rules = getProp<Arrayable<FormItemRule> | undefined>(
+      formRules,
+      props.prop
+    ).value
+    if (_rules) {
+      rules.push(...ensureArray(_rules))
+    }
+  }
+
+  if (required !== undefined) {
+    const requiredRules = rules
+      .map((rule, i) => [rule, i] as const)
+      .filter(([rule]) => Object.keys(rule).includes('required'))
+
+    if (requiredRules.length > 0) {
+      for (const [rule, i] of requiredRules) {
+        if (rule.required === required) continue
+        rules[i] = { ...rule, required }
+      }
+    } else {
+      rules.push({ required })
+    }
+  }
+
+  return rules
+})
+
+const validateEnabled = computed(() => normalizedRules.value.length > 0)
+
+const getFilteredRule = (trigger: string) => {
+  const rules = normalizedRules.value
+  return (
+    rules
+      .filter((rule) => {
+        if (!rule.trigger || !trigger) return true
+        if (Array.isArray(rule.trigger)) {
+          return rule.trigger.includes(trigger)
+        } else {
+          return rule.trigger === trigger
+        }
+      })
+      // exclude trigger
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      .map(({ trigger, ...rule }): RuleItem => rule)
+  )
+}
+
+const isRequired = computed(() =>
+  normalizedRules.value.some((rule) => rule.required)
+)
+
+const shouldShowError = computed(
+  () =>
+    validateStateDebounced.value === 'error' &&
+    props.showMessage &&
+    (formContext?.showMessage ?? true)
+)
+
+const currentLabel = computed(
+  () => `${props.label || ''}${formContext?.labelSuffix || ''}`
+)
+
+const setValidationState = (state: FormItemValidateState) => {
+  validateState.value = state
+}
+
+const onValidationFailed = (error: FormValidateFailure) => {
+  const { errors, fields } = error
+  if (!errors || !fields) {
+    console.error(error)
+  }
+
+  setValidationState('error')
+  validateMessage.value = errors
+    ? errors?.[0]?.message ?? `${props.prop} is required`
+    : ''
+
+  formContext?.emit('validate', props.prop!, false, validateMessage.value)
+}
+
+const onValidationSucceeded = () => {
+  setValidationState('success')
+  formContext?.emit('validate', props.prop!, true, '')
+}
+
+const doValidate = async (rules: RuleItem[]): Promise<true> => {
+  const modelName = propString.value
+  const validator = new AsyncValidator({
+    [modelName]: rules,
+  })
+  return validator
+    .validate({ [modelName]: fieldValue.value }, { firstFields: true })
+    .then(() => {
+      onValidationSucceeded()
+      return true as const
+    })
+    .catch((err: FormValidateFailure) => {
+      onValidationFailed(err as FormValidateFailure)
+      return Promise.reject(err)
+    })
+}
+
+const validate: FormItemContext['validate'] = async (trigger, callback) => {
+  // skip validation if its resetting
+  if (isResettingField || !props.prop) {
+    return false
+  }
+
+  const hasCallback = isFunction(callback)
+  if (!validateEnabled.value) {
+    callback?.(false)
+    return false
+  }
+
+  const rules = getFilteredRule(trigger)
+  if (rules.length === 0) {
+    callback?.(true)
+    return true
+  }
+
+  setValidationState('validating')
+
+  return doValidate(rules)
+    .then(() => {
+      callback?.(true)
+      return true as const
+    })
+    .catch((err: FormValidateFailure) => {
+      const { fields } = err
+      callback?.(false, fields)
+      return hasCallback ? false : Promise.reject(fields)
+    })
+}
+
+const clearValidate: FormItemContext['clearValidate'] = () => {
+  setValidationState('')
+  validateMessage.value = ''
+  isResettingField = false
+}
+
+const resetField: FormItemContext['resetField'] = async () => {
+  const model = formContext?.model
+  if (!model || !props.prop) return
+
+  const computedValue = getProp(model, props.prop)
+
+  // prevent validation from being triggered
+  isResettingField = true
+
+  computedValue.value = clone(initialValue)
+
+  await nextTick()
+  clearValidate()
+
+  isResettingField = false
+}
+
+const addInputId: FormItemContext['addInputId'] = (id: string) => {
+  if (!inputIds.value.includes(id)) {
+    inputIds.value.push(id)
+  }
+}
+
+const removeInputId: FormItemContext['removeInputId'] = (id: string) => {
+  inputIds.value = inputIds.value.filter((listId) => listId !== id)
+}
+
+watch(
+  () => props.error,
+  (val) => {
+    validateMessage.value = val || ''
+    setValidationState(val ? 'error' : '')
+  },
+  { immediate: true }
+)
+
+watch(
+  () => props.validateStatus,
+  (val) => setValidationState(val || '')
+)
+
+const context: FormItemContext = reactive({
+  ...toRefs(props),
+  $el: formItemRef,
+  size: _size,
+  validateState,
+  labelId,
+  inputIds,
+  isGroup,
+  hasLabel,
+  addInputId,
+  removeInputId,
+  resetField,
+  clearValidate,
+  validate,
+})
+
+provide(formItemContextKey, context)
+
+onMounted(() => {
+  if (props.prop) {
+    formContext?.addField(context)
+    initialValue = clone(fieldValue.value)
+  }
+})
+
+onBeforeUnmount(() => {
+  formContext?.removeField(context)
+})
+
+defineExpose({
+  /**
+   * @description Form item size.
+   */
+  size: _size,
+  /**
+   * @description Validation message.
+   */
+  validateMessage,
+  /**
+   * @description Validation state.
+   */
+  validateState,
+  /**
+   * @description Validate form item.
+   */
+  validate,
+  /**
+   * @description Remove validation status of the field.
+   */
+  clearValidate,
+  /**
+   * @description Reset current field and remove validation result.
+   */
+  resetField,
+})
+</script>

+ 117 - 0
packages/components/basic/form/src/form-label-wrap.tsx

@@ -0,0 +1,117 @@
+import {
+  Fragment,
+  computed,
+  defineComponent,
+  inject,
+  nextTick,
+  onBeforeUnmount,
+  onMounted,
+  onUpdated,
+  ref,
+  watch,
+} from 'vue'
+import { useResizeObserver } from '@vueuse/core'
+import { getStyle, throwError } from '@kankan-components/utils'
+import { useNamespace } from '@kankan-components/hooks'
+import { formContextKey, formItemContextKey } from './constants'
+
+import type { CSSProperties } from 'vue'
+
+const COMPONENT_NAME = 'ElLabelWrap'
+export default defineComponent({
+  name: COMPONENT_NAME,
+  props: {
+    isAutoWidth: Boolean,
+    updateAll: Boolean,
+  },
+
+  setup(props, { slots }) {
+    const formContext = inject(formContextKey, undefined)
+    const formItemContext = inject(formItemContextKey)
+    if (!formItemContext)
+      throwError(
+        COMPONENT_NAME,
+        'usage: <el-form-item><label-wrap /></el-form-item>'
+      )
+
+    const ns = useNamespace('form')
+
+    const el = ref<HTMLElement>()
+    const computedWidth = ref(0)
+
+    const getLabelWidth = () => {
+      if (el.value?.firstElementChild) {
+        const width = getStyle(
+          el.value.firstElementChild as HTMLElement,
+          'width'
+        )
+        return Math.ceil(Number.parseFloat(width)) || 0
+      } else {
+        return 0
+      }
+    }
+
+    const updateLabelWidth = (action: 'update' | 'remove' = 'update') => {
+      nextTick(() => {
+        if (slots.default && props.isAutoWidth) {
+          if (action === 'update') {
+            computedWidth.value = getLabelWidth()
+          } else if (action === 'remove') {
+            formContext?.deregisterLabelWidth(computedWidth.value)
+          }
+        }
+      })
+    }
+    const updateLabelWidthFn = () => updateLabelWidth('update')
+
+    onMounted(() => {
+      updateLabelWidthFn()
+    })
+    onBeforeUnmount(() => {
+      updateLabelWidth('remove')
+    })
+    onUpdated(() => updateLabelWidthFn())
+
+    watch(computedWidth, (val, oldVal) => {
+      if (props.updateAll) {
+        formContext?.registerLabelWidth(val, oldVal)
+      }
+    })
+
+    useResizeObserver(
+      computed(
+        () => (el.value?.firstElementChild ?? null) as HTMLElement | null
+      ),
+      updateLabelWidthFn
+    )
+
+    return () => {
+      if (!slots) return null
+
+      const { isAutoWidth } = props
+      if (isAutoWidth) {
+        const autoLabelWidth = formContext?.autoLabelWidth
+        const hasLabel = formItemContext?.hasLabel
+        const style: CSSProperties = {}
+        if (hasLabel && autoLabelWidth && autoLabelWidth !== 'auto') {
+          const marginWidth = Math.max(
+            0,
+            Number.parseInt(autoLabelWidth, 10) - computedWidth.value
+          )
+          const marginPosition =
+            formContext.labelPosition === 'left' ? 'marginRight' : 'marginLeft'
+          if (marginWidth) {
+            style[marginPosition] = `${marginWidth}px`
+          }
+        }
+        return (
+          <div ref={el} class={[ns.be('item', 'label-wrap')]} style={style}>
+            {slots.default?.()}
+          </div>
+        )
+      } else {
+        return <Fragment ref={el}>{slots.default?.()}</Fragment>
+      }
+    }
+  },
+})

+ 120 - 0
packages/components/basic/form/src/form.ts

@@ -0,0 +1,120 @@
+import { componentSizes } from '@kankan-components/constants'
+import {
+  buildProps,
+  definePropType,
+  isArray,
+  isBoolean,
+  isString,
+} from '@kankan-components/utils'
+
+import type { ExtractPropTypes } from 'vue'
+import type { FormItemProp } from './form-item'
+import type { FormRules } from './types'
+
+const formMetaProps = buildProps({
+  /**
+   * @description Control the size of components in this form.
+   */
+  size: {
+    type: String,
+    values: componentSizes,
+  },
+  /**
+   * @description Whether to disable all components in this form. If set to `true`, it will override the `disabled` prop of the inner component.
+   */
+  disabled: Boolean,
+} as const)
+
+export const formProps = buildProps({
+  ...formMetaProps,
+  /**
+   * @description Data of form component.
+   */
+  model: Object,
+  /**
+   * @description Validation rules of form.
+   */
+  rules: {
+    type: definePropType<FormRules>(Object),
+  },
+  /**
+   * @description Position of label. If set to `'left'` or `'right'`, `label-width` prop is also required.
+   */
+  labelPosition: {
+    type: String,
+    values: ['left', 'right', 'top'],
+    default: 'right',
+  },
+  /**
+   * @description Position of asterisk.
+   */
+  requireAsteriskPosition: {
+    type: String,
+    values: ['left', 'right'],
+    default: 'left',
+  },
+  /**
+   * @description Width of label, e.g. `'50px'`. All its direct child form items will inherit this value. `auto` is supported.
+   */
+  labelWidth: {
+    type: [String, Number],
+    default: '',
+  },
+  /**
+   * @description Suffix of the label.
+   */
+  labelSuffix: {
+    type: String,
+    default: '',
+  },
+  /**
+   * @description Whether the form is inline.
+   */
+  inline: Boolean,
+  /**
+   * @description Whether to display the error message inline with the form item.
+   */
+  inlineMessage: Boolean,
+  /**
+   * @description Whether to display an icon indicating the validation result.
+   */
+  statusIcon: Boolean,
+  /**
+   * @description Whether to show the error message.
+   */
+  showMessage: {
+    type: Boolean,
+    default: true,
+  },
+  /**
+   * @description Whether to trigger validation when the `rules` prop is changed.
+   */
+  validateOnRuleChange: {
+    type: Boolean,
+    default: true,
+  },
+  /**
+   * @description Whether to hide required fields should have a red asterisk (star) beside their labels.
+   */
+  hideRequiredAsterisk: Boolean,
+  /**
+   * @description When validation fails, scroll to the first error form entry.
+   */
+  scrollToError: Boolean,
+  /**
+   * @description When validation fails, it scrolls to the first error item based on the scrollIntoView option.
+   */
+  scrollIntoViewOptions: {
+    type: [Object, Boolean],
+  },
+} as const)
+export type FormProps = ExtractPropTypes<typeof formProps>
+export type FormMetaProps = ExtractPropTypes<typeof formMetaProps>
+
+export const formEmits = {
+  validate: (prop: FormItemProp, isValid: boolean, message: string) =>
+    (isArray(prop) || isString(prop)) &&
+    isBoolean(isValid) &&
+    isString(message),
+}
+export type FormEmits = typeof formEmits

+ 200 - 0
packages/components/basic/form/src/form.vue

@@ -0,0 +1,200 @@
+<template>
+  <form :class="formClasses">
+    <slot />
+  </form>
+</template>
+
+<script lang="ts" setup>
+import { computed, provide, reactive, toRefs, watch } from 'vue'
+import { debugWarn, isFunction } from '@kankan-components/utils'
+import { useNamespace } from '@kankan-components/hooks'
+import { useFormSize } from './hooks'
+import { formContextKey } from './constants'
+import { formEmits, formProps } from './form'
+import { filterFields, useFormLabelWidth } from './utils'
+
+import type { ValidateFieldsError } from 'async-validator'
+import type { Arrayable } from '@kankan-components/utils'
+import type {
+  FormContext,
+  FormItemContext,
+  FormValidateCallback,
+  FormValidationResult,
+} from './types'
+import type { FormItemProp } from './form-item'
+
+const COMPONENT_NAME = 'ElForm'
+defineOptions({
+  name: COMPONENT_NAME,
+})
+const props = defineProps(formProps)
+const emit = defineEmits(formEmits)
+
+const fields: FormItemContext[] = []
+
+const formSize = useFormSize()
+const ns = useNamespace('form')
+const formClasses = computed(() => {
+  const { labelPosition, inline } = props
+  return [
+    ns.b(),
+    // todo: in v2.2.0, we can remove default
+    // in fact, remove it doesn't affect the final style
+    ns.m(formSize.value || 'default'),
+    {
+      [ns.m(`label-${labelPosition}`)]: labelPosition,
+      [ns.m('inline')]: inline,
+    },
+  ]
+})
+
+const addField: FormContext['addField'] = (field) => {
+  fields.push(field)
+}
+
+const removeField: FormContext['removeField'] = (field) => {
+  if (field.prop) {
+    fields.splice(fields.indexOf(field), 1)
+  }
+}
+
+const resetFields: FormContext['resetFields'] = (properties = []) => {
+  if (!props.model) {
+    debugWarn(COMPONENT_NAME, 'model is required for resetFields to work.')
+    return
+  }
+  filterFields(fields, properties).forEach((field) => field.resetField())
+}
+
+const clearValidate: FormContext['clearValidate'] = (props = []) => {
+  filterFields(fields, props).forEach((field) => field.clearValidate())
+}
+
+const isValidatable = computed(() => {
+  const hasModel = !!props.model
+  if (!hasModel) {
+    debugWarn(COMPONENT_NAME, 'model is required for validate to work.')
+  }
+  return hasModel
+})
+
+const obtainValidateFields = (props: Arrayable<FormItemProp>) => {
+  if (fields.length === 0) return []
+
+  const filteredFields = filterFields(fields, props)
+  if (!filteredFields.length) {
+    debugWarn(COMPONENT_NAME, 'please pass correct props!')
+    return []
+  }
+  return filteredFields
+}
+
+const validate = async (
+  callback?: FormValidateCallback
+): FormValidationResult => validateField(undefined, callback)
+
+const doValidateField = async (
+  props: Arrayable<FormItemProp> = []
+): Promise<boolean> => {
+  if (!isValidatable.value) return false
+
+  const fields = obtainValidateFields(props)
+  if (fields.length === 0) return true
+
+  let validationErrors: ValidateFieldsError = {}
+  for (const field of fields) {
+    try {
+      await field.validate('')
+    } catch (fields) {
+      validationErrors = {
+        ...validationErrors,
+        ...(fields as ValidateFieldsError),
+      }
+    }
+  }
+
+  if (Object.keys(validationErrors).length === 0) return true
+  return Promise.reject(validationErrors)
+}
+
+const validateField: FormContext['validateField'] = async (
+  modelProps = [],
+  callback
+) => {
+  const shouldThrow = !isFunction(callback)
+  try {
+    const result = await doValidateField(modelProps)
+    // When result is false meaning that the fields are not validatable
+    if (result === true) {
+      callback?.(result)
+    }
+    return result
+  } catch (e) {
+    if (e instanceof Error) throw e
+
+    const invalidFields = e as ValidateFieldsError
+
+    if (props.scrollToError) {
+      scrollToField(Object.keys(invalidFields)[0])
+    }
+    callback?.(false, invalidFields)
+    return shouldThrow && Promise.reject(invalidFields)
+  }
+}
+
+const scrollToField = (prop: FormItemProp) => {
+  const field = filterFields(fields, prop)[0]
+  if (field) {
+    field.$el?.scrollIntoView(props.scrollIntoViewOptions)
+  }
+}
+
+watch(
+  () => props.rules,
+  () => {
+    if (props.validateOnRuleChange) {
+      validate().catch((err) => debugWarn(err))
+    }
+  },
+  { deep: true }
+)
+
+provide(
+  formContextKey,
+  reactive({
+    ...toRefs(props),
+    emit,
+
+    resetFields,
+    clearValidate,
+    validateField,
+    addField,
+    removeField,
+
+    ...useFormLabelWidth(),
+  })
+)
+
+defineExpose({
+  /**
+   * @description Validate the whole form. Receives a callback or returns `Promise`.
+   */
+  validate,
+  /**
+   * @description Validate specified fields.
+   */
+  validateField,
+  /**
+   * @description Reset specified fields and remove validation result.
+   */
+  resetFields,
+  /**
+   * @description Clear validation message for specified fields.
+   */
+  clearValidate,
+  /**
+   * @description Scroll to the specified fields.
+   */
+  scrollToField,
+})
+</script>

+ 2 - 0
packages/components/basic/form/src/hooks/index.ts

@@ -0,0 +1,2 @@
+export * from './use-form-common-props'
+export * from './use-form-item'

+ 44 - 0
packages/components/basic/form/src/hooks/use-form-common-props.ts

@@ -0,0 +1,44 @@
+import { computed, inject, ref, unref } from 'vue'
+import { useGlobalSize, useProp } from '@kankan-components/hooks'
+import { formContextKey, formItemContextKey } from '../constants'
+
+import type { ComponentSize } from '@kankan-components/constants'
+import type { MaybeRef } from '@vueuse/core'
+
+export const useFormSize = (
+  fallback?: MaybeRef<ComponentSize | undefined>,
+  ignore: Partial<Record<'prop' | 'form' | 'formItem' | 'global', boolean>> = {}
+) => {
+  const emptyRef = ref(undefined)
+
+  const size = ignore.prop ? emptyRef : useProp<ComponentSize>('size')
+  const globalConfig = ignore.global ? emptyRef : useGlobalSize()
+  const form = ignore.form
+    ? { size: undefined }
+    : inject(formContextKey, undefined)
+  const formItem = ignore.formItem
+    ? { size: undefined }
+    : inject(formItemContextKey, undefined)
+
+  return computed(
+    (): ComponentSize =>
+      size.value ||
+      unref(fallback) ||
+      formItem?.size ||
+      form?.size ||
+      globalConfig.value ||
+      ''
+  )
+}
+
+export const useFormDisabled = (fallback?: MaybeRef<boolean | undefined>) => {
+  const disabled = useProp<boolean>('disabled')
+  const form = inject(formContextKey, undefined)
+  return computed(
+    () => disabled.value || unref(fallback) || form?.disabled || false
+  )
+}
+
+// These exports are used for preventing breaking changes
+export const useSize = useFormSize
+export const useDisabled = useFormDisabled

+ 92 - 0
packages/components/basic/form/src/hooks/use-form-item.ts

@@ -0,0 +1,92 @@
+import {
+  computed,
+  inject,
+  onMounted,
+  onUnmounted,
+  ref,
+  toRef,
+  watch,
+} from 'vue'
+import { useId } from '@kankan-components/hooks'
+import { formContextKey, formItemContextKey } from '../constants'
+
+import type { ComputedRef, Ref, WatchStopHandle } from 'vue'
+import type { FormItemContext } from '../types'
+
+export const useFormItem = () => {
+  const form = inject(formContextKey, undefined)
+  const formItem = inject(formItemContextKey, undefined)
+  return {
+    form,
+    formItem,
+  }
+}
+
+export type IUseFormItemInputCommonProps = {
+  id?: string
+  label?: string | number | boolean | Record<string, any>
+}
+
+export const useFormItemInputId = (
+  props: Partial<IUseFormItemInputCommonProps>,
+  {
+    formItemContext,
+    disableIdGeneration,
+    disableIdManagement,
+  }: {
+    formItemContext?: FormItemContext
+    disableIdGeneration?: ComputedRef<boolean> | Ref<boolean>
+    disableIdManagement?: ComputedRef<boolean> | Ref<boolean>
+  }
+) => {
+  if (!disableIdGeneration) {
+    disableIdGeneration = ref<boolean>(false)
+  }
+  if (!disableIdManagement) {
+    disableIdManagement = ref<boolean>(false)
+  }
+
+  const inputId = ref<string>()
+  let idUnwatch: WatchStopHandle | undefined = undefined
+
+  const isLabeledByFormItem = computed<boolean>(() => {
+    return !!(
+      !props.label &&
+      formItemContext &&
+      formItemContext.inputIds &&
+      formItemContext.inputIds?.length <= 1
+    )
+  })
+
+  // Generate id for ElFormItem label if not provided as prop
+  onMounted(() => {
+    idUnwatch = watch(
+      [toRef(props, 'id'), disableIdGeneration] as any,
+      ([id, disableIdGeneration]: [string, boolean]) => {
+        const newId = id ?? (!disableIdGeneration ? useId().value : undefined)
+        if (newId !== inputId.value) {
+          if (formItemContext?.removeInputId) {
+            inputId.value && formItemContext.removeInputId(inputId.value)
+            if (!disableIdManagement?.value && !disableIdGeneration && newId) {
+              formItemContext.addInputId(newId)
+            }
+          }
+          inputId.value = newId
+        }
+      },
+      { immediate: true }
+    )
+  })
+
+  onUnmounted(() => {
+    idUnwatch && idUnwatch()
+    if (formItemContext?.removeInputId) {
+      inputId.value && formItemContext.removeInputId(inputId.value)
+    }
+  })
+
+  return {
+    isLabeledByFormItem,
+    inputId,
+  }
+}

+ 141 - 0
packages/components/basic/form/src/types.ts

@@ -0,0 +1,141 @@
+import type { SetupContext, UnwrapRef } from 'vue'
+import type {
+  RuleItem,
+  ValidateError,
+  ValidateFieldsError,
+} from 'async-validator'
+import type { ComponentSize } from '@kankan-components/constants'
+import type { Arrayable } from '@kankan-components/utils'
+import type { MaybeRef } from '@vueuse/core'
+import type {
+  FormItemProp,
+  FormItemProps,
+  FormItemValidateState,
+} from './form-item'
+import type { FormEmits, FormProps } from './form'
+
+import type { useFormLabelWidth } from './utils'
+
+export type FormLabelWidthContext = ReturnType<typeof useFormLabelWidth>
+export interface FormItemRule extends RuleItem {
+  trigger?: Arrayable<string>
+}
+
+type Primitive = null | undefined | string | number | boolean | symbol | bigint
+/**
+ * Check whether it is tuple
+ *
+ * 检查是否为元组
+ *
+ * @example
+ * IsTuple<[1, 2, 3]> => true
+ * IsTuple<Array[number]> => false
+ */
+type IsTuple<T extends ReadonlyArray<any>> = number extends T['length']
+  ? false
+  : true
+/**
+ * Array method key
+ *
+ * 数组方法键
+ */
+type ArrayMethodKey = keyof any[]
+/**
+ * Tuple index key
+ *
+ * 元组下标键
+ *
+ * @example
+ * TupleKey<[1, 2, 3]> => '0' | '1' | '2'
+ */
+type TupleKey<T extends ReadonlyArray<any>> = Exclude<keyof T, ArrayMethodKey>
+/**
+ * Array index key
+ *
+ * 数组下标键
+ */
+type ArrayKey = number
+/**
+ * Helper type for recursively constructing paths through a type
+ *
+ * 用于通过一个类型递归构建路径的辅助类型
+ */
+type PathImpl<K extends string | number, V> = V extends Primitive
+  ? `${K}`
+  : `${K}` | `${K}.${Path<V>}`
+/**
+ * Type which collects all paths through a type
+ *
+ * 通过一个类型收集所有路径的类型
+ *
+ * @see {@link FieldPath}
+ */
+type Path<T> = T extends ReadonlyArray<infer V>
+  ? IsTuple<T> extends true
+    ? {
+        [K in TupleKey<T>]-?: PathImpl<Exclude<K, symbol>, T[K]>
+      }[TupleKey<T>] // tuple
+    : PathImpl<ArrayKey, V> // array
+  : {
+      [K in keyof T]-?: PathImpl<Exclude<K, symbol>, T[K]>
+    }[keyof T] // object
+/**
+ * Type which collects all paths through a type
+ *
+ * 通过一个类型收集所有路径的类型
+ *
+ * @example
+ * FieldPath<{ 1: number; a: number; b: string; c: { d: number; e: string }; f: [{ value: string }]; g: { value: string }[] }> => '1' | 'a' | 'b' | 'c' | 'f' | 'g' | 'c.d' | 'c.e' | 'f.0' | 'f.0.value' | 'g.number' | 'g.number.value'
+ */
+type FieldPath<T> = T extends object ? Path<T> : never
+export type FormRules<
+  T extends MaybeRef<Record<string, any> | string> = string
+> = Partial<
+  Record<
+    UnwrapRef<T> extends string ? UnwrapRef<T> : FieldPath<UnwrapRef<T>>,
+    Arrayable<FormItemRule>
+  >
+>
+
+export type FormValidationResult = Promise<boolean>
+export type FormValidateCallback = (
+  isValid: boolean,
+  invalidFields?: ValidateFieldsError
+) => void
+export interface FormValidateFailure {
+  errors: ValidateError[] | null
+  fields: ValidateFieldsError
+}
+
+export type FormContext = FormProps &
+  UnwrapRef<FormLabelWidthContext> & {
+    emit: SetupContext<FormEmits>['emit']
+
+    // expose
+    addField: (field: FormItemContext) => void
+    removeField: (field: FormItemContext) => void
+    resetFields: (props?: Arrayable<FormItemProp>) => void
+    clearValidate: (props?: Arrayable<FormItemProp>) => void
+    validateField: (
+      props?: Arrayable<FormItemProp>,
+      callback?: FormValidateCallback
+    ) => FormValidationResult
+  }
+
+export interface FormItemContext extends FormItemProps {
+  $el: HTMLDivElement | undefined
+  size: ComponentSize
+  validateState: FormItemValidateState
+  isGroup: boolean
+  labelId: string
+  inputIds: string[]
+  hasLabel: boolean
+  addInputId: (id: string) => void
+  removeInputId: (id: string) => void
+  validate: (
+    trigger: string,
+    callback?: FormValidateCallback
+  ) => FormValidationResult
+  resetField(): void
+  clearValidate(): void
+}

+ 57 - 0
packages/components/basic/form/src/utils.ts

@@ -0,0 +1,57 @@
+import { computed, ref } from 'vue'
+import { debugWarn, ensureArray } from '@kankan-components/utils'
+import type { Arrayable } from '@kankan-components/utils'
+import type { FormItemContext } from './types'
+import type { FormItemProp } from './form-item'
+
+const SCOPE = 'ElForm'
+
+export function useFormLabelWidth() {
+  const potentialLabelWidthArr = ref<number[]>([])
+
+  const autoLabelWidth = computed(() => {
+    if (!potentialLabelWidthArr.value.length) return '0'
+    const max = Math.max(...potentialLabelWidthArr.value)
+    return max ? `${max}px` : ''
+  })
+
+  function getLabelWidthIndex(width: number) {
+    const index = potentialLabelWidthArr.value.indexOf(width)
+    if (index === -1 && autoLabelWidth.value === '0') {
+      debugWarn(SCOPE, `unexpected width ${width}`)
+    }
+    return index
+  }
+
+  function registerLabelWidth(val: number, oldVal: number) {
+    if (val && oldVal) {
+      const index = getLabelWidthIndex(oldVal)
+      potentialLabelWidthArr.value.splice(index, 1, val)
+    } else if (val) {
+      potentialLabelWidthArr.value.push(val)
+    }
+  }
+
+  function deregisterLabelWidth(val: number) {
+    const index = getLabelWidthIndex(val)
+    if (index > -1) {
+      potentialLabelWidthArr.value.splice(index, 1)
+    }
+  }
+
+  return {
+    autoLabelWidth,
+    registerLabelWidth,
+    deregisterLabelWidth,
+  }
+}
+
+export const filterFields = (
+  fields: FormItemContext[],
+  props: Arrayable<FormItemProp>
+) => {
+  const normalized = ensureArray(props)
+  return normalized.length > 0
+    ? fields.filter((field) => field.prop && normalized.includes(field.prop))
+    : fields
+}

+ 2 - 0
packages/components/basic/form/style/css.ts

@@ -0,0 +1,2 @@
+import '@kankan-components/components/base/style/css'
+import '@kankan-components/theme-chalk/el-form.css'

+ 2 - 0
packages/components/basic/form/style/index.ts

@@ -0,0 +1,2 @@
+import '@kankan-components/components/base/style'
+import '@kankan-components/theme-chalk/src/form.scss'

+ 2 - 0
packages/components/basic/index.ts

@@ -7,3 +7,5 @@ export * from './dialog'
 export * from './config-provider'
 export * from './focus-trap'
 export * from './message-box'
+export * from './message'
+export * from './form'

+ 1 - 2
packages/components/basic/input/index.ts

@@ -6,5 +6,4 @@ export const KkInput = withInstall(Input)
 export default KkInput
 
 export * from './src/input'
-
-export type InputInstance = InstanceType<typeof Input>
+export type { InputInstance } from './src/instance'

+ 2 - 2
packages/components/basic/input/src/input.ts

@@ -160,7 +160,7 @@ export const inputProps = buildProps({
     default: true,
   },
   /**
-   * @description input or texearea element style
+   * @description input or textarea element style
    */
   inputStyle: {
     type: definePropType<StyleValue>([Object, Array, String]),
@@ -179,7 +179,7 @@ export const inputEmits = {
   mouseleave: (evt: MouseEvent) => evt instanceof MouseEvent,
   mouseenter: (evt: MouseEvent) => evt instanceof MouseEvent,
   // NOTE: when autofill by browser, the keydown event is instanceof Event, not KeyboardEvent
-  // relative bug report https://github.com/kankan-components/kankan-components/issues/6665
+  // relative bug report https://github.com/element-plus/element-plus/issues/6665
   keydown: (evt: KeyboardEvent | Event) => evt instanceof Event,
   compositionstart: (evt: CompositionEvent) => evt instanceof CompositionEvent,
   compositionupdate: (evt: CompositionEvent) => evt instanceof CompositionEvent,

+ 83 - 55
packages/components/basic/input/src/input.vue

@@ -15,18 +15,19 @@
         <slot name="prepend" />
       </div>
 
-      <div :class="wrapperKls">
+      <div ref="wrapperRef" :class="wrapperKls">
         <!-- prefix slot -->
         <span v-if="$slots.prefix || prefixIcon" :class="nsInput.e('prefix')">
-          <span :class="nsInput.e('prefix-inner')" @click="focus">
+          <span :class="nsInput.e('prefix-inner')">
             <slot name="prefix" />
             <kk-icon v-if="prefixIcon" :class="nsInput.e('icon')">
               <component :is="prefixIcon" />
             </kk-icon>
           </span>
         </span>
-        <!-- :id="inputId" -->
+
         <input
+          :id="inputId"
           ref="input"
           :class="nsInput.e('inner')"
           v-bind="attrs"
@@ -53,7 +54,7 @@
 
         <!-- suffix slot -->
         <span v-if="suffixVisible" :class="nsInput.e('suffix')">
-          <span :class="nsInput.e('suffix-inner')" @click="focus">
+          <span :class="nsInput.e('suffix-inner')">
             <template
               v-if="!showClear || !showPwdVisible || !isWordLimitVisible"
             >
@@ -104,8 +105,8 @@
 
     <!-- textarea -->
     <template v-else>
-      <!-- :id="inputId" -->
       <textarea
+        :id="inputId"
         ref="textarea"
         :class="nsTextarea.e('inner')"
         v-bind="attrs"
@@ -149,7 +150,7 @@ import {
   useSlots,
   watch,
 } from 'vue'
-import { isClient, useResizeObserver } from '@vueuse/core'
+import { useResizeObserver } from '@vueuse/core'
 import { isNil } from 'lodash-unified'
 import { KkIcon } from '@kankan-components/components/basic/icon'
 import {
@@ -157,22 +158,25 @@ import {
   Hide as IconHide,
   View as IconView,
 } from '@kankan-components/icons-vue'
-
+import {
+  useFormDisabled,
+  useFormItem,
+  useFormItemInputId,
+  useFormSize,
+} from '@kankan-components/components/basic/form'
 import {
   NOOP,
-  // ValidateComponentsMap,
+  ValidateComponentsMap,
   debugWarn,
+  isClient,
   isKorean,
   isObject,
 } from '@kankan-components/utils'
 import {
   useAttrs,
   useCursor,
-  useDisabled,
-  // useFormItem,
-  // useFormItemInputId,
+  useFocusController,
   useNamespace,
-  useSize,
 } from '@kankan-components/hooks'
 import { UPDATE_MODEL_EVENT } from '@kankan-components/constants'
 import { calcTextareaHeight } from './utils'
@@ -182,7 +186,7 @@ import type { StyleValue } from 'vue'
 type TargetElement = HTMLInputElement | HTMLTextAreaElement
 
 defineOptions({
-  name: 'KkInput',
+  name: 'ElInput',
   inheritAttrs: false,
 })
 const props = defineProps(inputProps)
@@ -221,7 +225,7 @@ const containerKls = computed(() => [
 
 const wrapperKls = computed(() => [
   nsInput.e('wrapper'),
-  nsInput.is('focus', focused.value),
+  nsInput.is('focus', isFocused.value),
 ])
 
 const attrs = useAttrs({
@@ -229,19 +233,18 @@ const attrs = useAttrs({
     return Object.keys(containerAttrs.value)
   }),
 })
-// const { form, formItem } = useFormItem()
-// const { inputId } = useFormItemInputId(props, {
-//   formItemContext: formItem,
-// })
-const inputSize = useSize()
-const inputDisabled = useDisabled()
+const { form, formItem } = useFormItem()
+const { inputId } = useFormItemInputId(props, {
+  formItemContext: formItem,
+})
+const inputSize = useFormSize()
+const inputDisabled = useFormDisabled()
 const nsInput = useNamespace('input')
 const nsTextarea = useNamespace('textarea')
 
 const input = shallowRef<HTMLInputElement>()
 const textarea = shallowRef<HTMLTextAreaElement>()
 
-const focused = ref(false)
 const hovering = ref(false)
 const isComposing = ref(false)
 const passwordVisible = ref(false)
@@ -250,14 +253,22 @@ const textareaCalcStyle = shallowRef(props.inputStyle)
 
 const _ref = computed(() => input.value || textarea.value)
 
-// const needStatusIcon = computed(() => form?.statusIcon ?? false)
-// const validateState = computed(() => formItem?.validateState || '')
-// const validateIcon = computed(
-//   () => validateState.value && ValidateComponentsMap[validateState.value]
-// )
-const needStatusIcon = computed(() => false)
-const validateState = computed(() => '')
-const validateIcon = computed(() => validateState.value)
+const { wrapperRef, isFocused, handleFocus, handleBlur } = useFocusController(
+  _ref,
+  {
+    afterBlur() {
+      if (props.validateEvent) {
+        formItem?.validate?.('blur').catch((err) => debugWarn(err))
+      }
+    },
+  }
+)
+
+const needStatusIcon = computed(() => form?.statusIcon ?? false)
+const validateState = computed(() => formItem?.validateState || '')
+const validateIcon = computed(
+  () => validateState.value && ValidateComponentsMap[validateState.value]
+)
 const passwordIcon = computed(() =>
   passwordVisible.value ? IconView : IconHide
 )
@@ -279,7 +290,7 @@ const showClear = computed(
     !inputDisabled.value &&
     !props.readonly &&
     !!nativeInputValue.value &&
-    (focused.value || hovering.value)
+    (isFocused.value || hovering.value)
 )
 const showPwdVisible = computed(
   () =>
@@ -287,7 +298,7 @@ const showPwdVisible = computed(
     !inputDisabled.value &&
     !props.readonly &&
     !!nativeInputValue.value &&
-    (!!nativeInputValue.value || focused.value)
+    (!!nativeInputValue.value || isFocused.value)
 )
 const isWordLimitVisible = computed(
   () =>
@@ -298,7 +309,7 @@ const isWordLimitVisible = computed(
     !props.readonly &&
     !props.showPassword
 )
-const textLength = computed(() => Array.from(nativeInputValue.value).length)
+const textLength = computed(() => nativeInputValue.value.length)
 const inputExceed = computed(
   () =>
     // show exceed style if length of initial value greater then maxlength
@@ -318,6 +329,7 @@ const suffixVisible = computed(
 const [recordCursor, setCursor] = useCursor(input)
 
 useResizeObserver(textarea, (entries) => {
+  onceInitSizeTextarea()
   if (!isWordLimitVisible.value || props.resize !== 'both') return
   const entry = entries[0]
   const { width } = entry.contentRect
@@ -330,25 +342,55 @@ useResizeObserver(textarea, (entries) => {
 const resizeTextarea = () => {
   const { type, autosize } = props
 
-  if (!isClient || type !== 'textarea') return
+  if (!isClient || type !== 'textarea' || !textarea.value) return
 
   if (autosize) {
     const minRows = isObject(autosize) ? autosize.minRows : undefined
     const maxRows = isObject(autosize) ? autosize.maxRows : undefined
+    const textareaStyle = calcTextareaHeight(textarea.value, minRows, maxRows)
+
+    // If the scrollbar is displayed, the height of the textarea needs more space than the calculated height.
+    // If set textarea height in this case, the scrollbar will not hide.
+    // So we need to hide scrollbar first, and reset it in next tick.
+    // see https://github.com/element-plus/element-plus/issues/8825
     textareaCalcStyle.value = {
-      ...calcTextareaHeight(textarea.value!, minRows, maxRows),
+      overflowY: 'hidden',
+      ...textareaStyle,
     }
+
+    nextTick(() => {
+      // NOTE: Force repaint to make sure the style set above is applied.
+      textarea.value!.offsetHeight
+      textareaCalcStyle.value = textareaStyle
+    })
   } else {
     textareaCalcStyle.value = {
-      minHeight: calcTextareaHeight(textarea.value!).minHeight,
+      minHeight: calcTextareaHeight(textarea.value).minHeight,
+    }
+  }
+}
+
+const createOnceInitResize = (resizeTextarea: () => void) => {
+  let isInit = false
+  return () => {
+    if (isInit || !props.autosize) return
+    const isElHidden = textarea.value?.offsetParent === null
+    if (!isElHidden) {
+      resizeTextarea()
+      isInit = true
     }
   }
 }
+// fix: https://github.com/element-plus/element-plus/issues/12074
+const onceInitSizeTextarea = createOnceInitResize(resizeTextarea)
 
 const setNativeInputValue = () => {
   const input = _ref.value
-  if (!input || input.value === nativeInputValue.value) return
-  input.value = nativeInputValue.value
+  const formatterValue = props.formatter
+    ? props.formatter(nativeInputValue.value)
+    : nativeInputValue.value
+  if (!input || input.value === formatterValue) return
+  input.value = formatterValue
 }
 
 const handleInput = async (event: Event) => {
@@ -358,7 +400,6 @@ const handleInput = async (event: Event) => {
 
   if (props.formatter) {
     value = props.parser ? props.parser(value) : value
-    value = props.formatter(value)
   }
 
   // should not emit input during composition
@@ -419,19 +460,6 @@ const focus = async () => {
 
 const blur = () => _ref.value?.blur()
 
-const handleFocus = (event: FocusEvent) => {
-  focused.value = true
-  emit('focus', event)
-}
-
-const handleBlur = (event: FocusEvent) => {
-  focused.value = false
-  emit('blur', event)
-  // if (props.validateEvent) {
-  //   formItem?.validate?.('blur').catch((err) => debugWarn(err))
-  // }
-}
-
 const handleMouseLeave = (evt: MouseEvent) => {
   hovering.value = false
   emit('mouseleave', evt)
@@ -461,9 +489,9 @@ watch(
   () => props.modelValue,
   () => {
     nextTick(() => resizeTextarea())
-    // if (props.validateEvent) {
-    //   formItem?.validate?.('change').catch((err) => debugWarn(err))
-    // }
+    if (props.validateEvent) {
+      formItem?.validate?.('change').catch((err) => debugWarn(err))
+    }
   }
 )
 
@@ -487,7 +515,7 @@ watch(
 onMounted(() => {
   if (!props.formatter && props.parser) {
     debugWarn(
-      'KkInput',
+      'ElInput',
       'If you set the parser, you also need to set the formatter.'
     )
   }

+ 3 - 0
packages/components/basic/input/src/instance.ts

@@ -0,0 +1,3 @@
+import type Input from './input.vue'
+
+export type InputInstance = InstanceType<typeof Input>

+ 2 - 2
packages/components/basic/input/src/utils.ts

@@ -1,11 +1,11 @@
-import { isNumber } from '@kankan-components/utils'
+import { isFirefox, isNumber } from '@kankan-components/utils'
 
 let hiddenTextarea: HTMLTextAreaElement | undefined = undefined
 
 const HIDDEN_STYLE = `
   height:0 !important;
   visibility:hidden !important;
-  overflow:hidden !important;
+  ${isFirefox() ? '' : 'overflow:hidden !important;'}
   position:absolute !important;
   z-index:-1000 !important;
   top:0 !important;

+ 33 - 36
packages/components/basic/message-box/src/index.vue

@@ -49,12 +49,11 @@
                 </kk-icon>
                 <span>{{ title }}</span>
               </div>
-              <!-- t('el.messagebox.close') -->
               <button
                 v-if="showClose"
                 type="button"
                 :class="ns.e('headerbtn')"
-                :aria-label="''"
+                :aria-label="t('el.messagebox.close')"
                 @click="
                   handleAction(distinguishCancelAndClose ? 'close' : 'cancel')
                 "
@@ -116,16 +115,15 @@
             </div>
             <div :class="ns.e('btns')">
               <kk-button
-                v-if="showCancelButton"
-                :loading="cancelButtonLoading"
-                :class="[cancelButtonClass]"
+                v-if="showCancKkButton"
+                :loading="cancKkButtonLoading"
+                :class="[cancKkButtonClass]"
                 :round="roundButton"
                 :size="btnSize"
                 @click="handleAction('cancel')"
                 @keydown.prevent.enter="handleAction('cancel')"
               >
-                <!-- // t('el.messagebox.cancel') -->
-                {{ cancelButtonText || '' }}
+                {{ cancKkButtonText || t('el.messagebox.cancel') }}
               </kk-button>
               <kk-button
                 v-show="showConfirmButton"
@@ -139,8 +137,7 @@
                 @click="handleAction('confirm')"
                 @keydown.prevent.enter="handleAction('confirm')"
               >
-                <!-- //t('el.messagebox.confirm') -->
-                {{ confirmButtonText || '' }}
+                {{ confirmButtonText || t('el.messagebox.confirm') }}
               </kk-button>
             </div>
           </div>
@@ -167,13 +164,8 @@ import { TrapFocus } from '@kankan-components/directives'
 import {
   useDraggable,
   useId,
-  // useLocale,
   useLockscreen,
-  useNamespace,
-  useRestoreActive,
   useSameTarget,
-  useSize,
-  useZIndex,
 } from '@kankan-components/hooks'
 import KkInput from '@kankan-components/components/basic/input'
 import { KkOverlay } from '@kankan-components/components/basic/overlay'
@@ -183,9 +175,10 @@ import {
   isValidComponentSize,
 } from '@kankan-components/utils'
 import { KkIcon } from '@kankan-components/components/basic/icon'
-import ElFocusTrap from '@kankan-components/components/basic/focus-trap'
+import KkFocusTrap from '@kankan-components/components/basic/focus-trap'
+import { useGlobalComponentSettings } from '@kankan-components/components/config-provider'
 
-import type { ComponentPublicInstance, PropType } from 'vue'
+import type { ComponentPublicInstance, DefineComponent, PropType } from 'vue'
 import type { ComponentSize } from '@kankan-components/constants'
 import type {
   Action,
@@ -200,7 +193,7 @@ export default defineComponent({
   },
   components: {
     KkButton,
-    ElFocusTrap,
+    KkFocusTrap,
     KkInput,
     KkOverlay,
     KkIcon,
@@ -254,18 +247,28 @@ export default defineComponent({
   emits: ['vanish', 'action'],
   setup(props, { emit }) {
     // const popup = usePopup(props, doClose)
-    // const { t } = useLocale()
-    const ns = useNamespace('message-box')
+    const {
+      locale,
+      zIndex,
+      ns,
+      size: btnSize,
+    } = useGlobalComponentSettings(
+      'message-box',
+      computed(() => props.buttonSize)
+    )
+
+    const { t } = locale
+    const { nextZIndex } = zIndex
+
     const visible = ref(false)
-    const { nextZIndex } = useZIndex()
     // s represents state
     const state = reactive<MessageBoxState>({
       // autofocus element when open message-box
       autofocus: true,
       beforeClose: null,
       callback: null,
-      cancelButtonText: '',
-      cancelButtonClass: '',
+      cancKkButtonText: '',
+      cancKkButtonClass: '',
       confirmButtonText: '',
       confirmButtonClass: '',
       customClass: '',
@@ -282,14 +285,14 @@ export default defineComponent({
       message: null,
       modalFade: true,
       modalClass: '',
-      showCancelButton: false,
+      showCancKkButton: false,
       showConfirmButton: true,
       type: '',
       title: undefined,
       showInput: false,
       action: '' as Action,
       confirmButtonLoading: false,
-      cancelButtonLoading: false,
+      cancKkButtonLoading: false,
       confirmButtonDisabled: false,
       editorErrorMessage: '',
       // refer to: https://github.com/ElemeFE/element/commit/2999279ae34ef10c373ca795c87b020ed6753eed
@@ -307,11 +310,6 @@ export default defineComponent({
     const contentId = useId()
     const inputId = useId()
 
-    const btnSize = useSize(
-      computed(() => props.buttonSize),
-      { prop: true, form: true, formItem: true }
-    )
-
     const iconComponent = computed(
       () => state.icon || TypeComponentsMap[state.type] || ''
     )
@@ -423,7 +421,8 @@ export default defineComponent({
       if (props.boxType === 'prompt') {
         const inputPattern = state.inputPattern
         if (inputPattern && !inputPattern.test(state.inputValue || '')) {
-          state.editorErrorMessage = state.inputErrorMessage || '' //t('el.messagebox.error')
+          state.editorErrorMessage =
+            state.inputErrorMessage || t('el.messagebox.error')
           state.validateError = true
           return false
         }
@@ -431,7 +430,8 @@ export default defineComponent({
         if (typeof inputValidator === 'function') {
           const validateResult = inputValidator(state.inputValue)
           if (validateResult === false) {
-            state.editorErrorMessage = state.inputErrorMessage || '' // t('el.messagebox.error')
+            state.editorErrorMessage =
+              state.inputErrorMessage || t('el.messagebox.error')
             state.validateError = true
             return false
           }
@@ -473,9 +473,6 @@ export default defineComponent({
       useLockscreen(visible)
     }
 
-    // restore to prev active element.
-    useRestoreActive(visible)
-
     return {
       ...toRefs(state),
       ns,
@@ -499,8 +496,8 @@ export default defineComponent({
       handleWrapperClick,
       handleInputEnter,
       handleAction,
-      // t,
+      t,
     }
   },
-})
+}) as DefineComponent
 </script>

+ 13 - 13
packages/components/basic/message-box/src/message-box.type.ts

@@ -62,7 +62,7 @@ export type Callback =
   | ((action: Action) => any)
 
 /** Options used in MessageBox */
-export interface KkMessageBoxOptions {
+export interface ElMessageBoxOptions {
   /**
    * auto focus when open message-box
    */
@@ -106,7 +106,7 @@ export interface KkMessageBoxOptions {
   message?: string | VNode | (() => VNode)
 
   /** Title of the MessageBox */
-  title?: string | KkMessageBoxOptions
+  title?: string | ElMessageBoxOptions
 
   /** Message type, used for icon display */
   type?: MessageType
@@ -175,19 +175,19 @@ export interface KkMessageBoxOptions {
   appendTo?: HTMLElement | string
 }
 
-export type KkMessageBoxShortcutMethod = ((
-  message: KkMessageBoxOptions['message'],
-  title: KkMessageBoxOptions['title'],
-  options?: KkMessageBoxOptions,
+export type ElMessageBoxShortcutMethod = ((
+  message: ElMessageBoxOptions['message'],
+  title: ElMessageBoxOptions['title'],
+  options?: ElMessageBoxOptions,
   appContext?: AppContext | null
 ) => Promise<MessageBoxData>) &
   ((
-    message: KkMessageBoxOptions['message'],
-    options?: KkMessageBoxOptions,
+    message: ElMessageBoxOptions['message'],
+    options?: ElMessageBoxOptions,
     appContext?: AppContext | null
   ) => Promise<MessageBoxData>)
 
-export interface IKkMessageBox {
+export interface IElMessageBox {
   _context: AppContext | null
 
   /** Show a message box */
@@ -195,18 +195,18 @@ export interface IKkMessageBox {
 
   /** Show a message box */
   (
-    options: KkMessageBoxOptions,
+    options: ElMessageBoxOptions,
     appContext?: AppContext | null
   ): Promise<MessageBoxData>
 
   /** Show an alert message box */
-  alert: KkMessageBoxShortcutMethod
+  alert: ElMessageBoxShortcutMethod
 
   /** Show a confirm message box */
-  confirm: KkMessageBoxShortcutMethod
+  confirm: ElMessageBoxShortcutMethod
 
   /** Show a prompt message box */
-  prompt: KkMessageBoxShortcutMethod
+  prompt: ElMessageBoxShortcutMethod
 
   /** Close current message box */
   close(): void

+ 17 - 17
packages/components/basic/message-box/src/messageBox.ts

@@ -1,8 +1,8 @@
 import { createVNode, render } from 'vue'
-import { isClient } from '@vueuse/core'
 import {
   debugWarn,
   hasOwn,
+  isClient,
   isElement,
   isFunction,
   isObject,
@@ -16,9 +16,9 @@ import type { AppContext, ComponentPublicInstance, VNode } from 'vue'
 import type {
   Action,
   Callback,
-  IKkMessageBox,
-  KkMessageBoxOptions,
-  KkMessageBoxShortcutMethod,
+  ElMessageBoxOptions,
+  ElMessageBoxShortcutMethod,
+  IElMessageBox,
   MessageBoxData,
   MessageBoxState,
 } from './message-box.type'
@@ -143,11 +143,11 @@ const showMessage = (options: any, appContext?: AppContext | null) => {
 }
 
 async function MessageBox(
-  options: KkMessageBoxOptions,
+  options: ElMessageBoxOptions,
   appContext?: AppContext | null
 ): Promise<MessageBoxData>
 function MessageBox(
-  options: KkMessageBoxOptions | string | VNode,
+  options: ElMessageBoxOptions | string | VNode,
   appContext: AppContext | null = null
 ): Promise<{ value: string; action: Action } | Action> {
   if (!isClient) return Promise.reject()
@@ -163,7 +163,7 @@ function MessageBox(
   return new Promise((resolve, reject) => {
     const vm = showMessage(
       options,
-      appContext ?? (MessageBox as IKkMessageBox)._context
+      appContext ?? (MessageBox as IElMessageBox)._context
     )
     // collect this vm in order to handle upcoming events.
     messageInstance.set(vm, {
@@ -177,8 +177,8 @@ function MessageBox(
 
 const MESSAGE_BOX_VARIANTS = ['alert', 'confirm', 'prompt'] as const
 const MESSAGE_BOX_DEFAULT_OPTS: Record<
-  (typeof MESSAGE_BOX_VARIANTS)[number],
-  Partial<KkMessageBoxOptions>
+  typeof MESSAGE_BOX_VARIANTS[number],
+  Partial<ElMessageBoxOptions>
 > = {
   alert: { closeOnPressEscape: false, closeOnClickModal: false },
   confirm: { showCancelButton: true },
@@ -186,21 +186,21 @@ const MESSAGE_BOX_DEFAULT_OPTS: Record<
 }
 
 MESSAGE_BOX_VARIANTS.forEach((boxType) => {
-  ;(MessageBox as IKkMessageBox)[boxType] = messageBoxFactory(
+  ;(MessageBox as IElMessageBox)[boxType] = messageBoxFactory(
     boxType
-  ) as KkMessageBoxShortcutMethod
+  ) as ElMessageBoxShortcutMethod
 })
 
-function messageBoxFactory(boxType: (typeof MESSAGE_BOX_VARIANTS)[number]) {
+function messageBoxFactory(boxType: typeof MESSAGE_BOX_VARIANTS[number]) {
   return (
     message: string | VNode,
-    title: string | KkMessageBoxOptions,
-    options?: KkMessageBoxOptions,
+    title: string | ElMessageBoxOptions,
+    options?: ElMessageBoxOptions,
     appContext?: AppContext | null
   ) => {
     let titleOrOpts = ''
     if (isObject(title)) {
-      options = title as KkMessageBoxOptions
+      options = title as ElMessageBoxOptions
       titleOrOpts = ''
     } else if (isUndefined(title)) {
       titleOrOpts = ''
@@ -236,6 +236,6 @@ MessageBox.close = () => {
 
   messageInstance.clear()
 }
-;(MessageBox as IKkMessageBox)._context = null
+;(MessageBox as IElMessageBox)._context = null
 
-export default MessageBox as IKkMessageBox
+export default MessageBox as IElMessageBox

+ 7 - 0
packages/components/basic/message/index.ts

@@ -0,0 +1,7 @@
+import { withInstallFunction } from '@kankan-components/utils'
+import Message from './src/method'
+
+export const KkMessage = withInstallFunction(Message, '$message')
+export default KkMessage
+
+export * from './src/message'

+ 35 - 0
packages/components/basic/message/src/instance.ts

@@ -0,0 +1,35 @@
+import { shallowReactive } from 'vue'
+import type { ComponentInternalInstance, VNode } from 'vue'
+import type { Mutable } from '@kankan-components/utils'
+import type { MessageHandler, MessageProps } from './message'
+
+export type MessageContext = {
+  id: string
+  vnode: VNode
+  handler: MessageHandler
+  vm: ComponentInternalInstance
+  props: Mutable<MessageProps>
+}
+
+export const instances: MessageContext[] = shallowReactive([])
+
+export const getInstance = (id: string) => {
+  const idx = instances.findIndex((instance) => instance.id === id)
+  const current = instances[idx]
+  let prev: MessageContext | undefined
+  if (idx > 0) {
+    prev = instances[idx - 1]
+  }
+  return { current, prev }
+}
+
+export const getLastOffset = (id: string): number => {
+  const { prev } = getInstance(id)
+  if (!prev) return 0
+  return prev.vm.exposed!.bottom.value
+}
+
+export const getOffsetOrSpace = (id: string, offset: number) => {
+  const idx = instances.findIndex((instance) => instance.id === id)
+  return idx > 0 ? 20 : offset
+}

+ 192 - 0
packages/components/basic/message/src/message.ts

@@ -0,0 +1,192 @@
+import {
+  buildProps,
+  definePropType,
+  iconPropType,
+  isClient,
+  mutable,
+} from '@kankan-components/utils'
+import type { AppContext, ExtractPropTypes, VNode } from 'vue'
+import type { Mutable } from '@kankan-components/utils'
+import type MessageConstructor from './message.vue'
+
+export const messageTypes = ['success', 'info', 'warning', 'error'] as const
+
+export type messageType = typeof messageTypes[number]
+
+export interface MessageConfigContext {
+  max?: number
+}
+
+export const messageDefaults = mutable({
+  customClass: '',
+  center: false,
+  dangerouslyUseHTMLString: false,
+  duration: 3000,
+  icon: undefined,
+  id: '',
+  message: '',
+  onClose: undefined,
+  showClose: false,
+  type: 'info',
+  offset: 16,
+  zIndex: 0,
+  grouping: false,
+  repeatNum: 1,
+  appendTo: isClient ? document.body : (undefined as never),
+} as const)
+
+export const messageProps = buildProps({
+  /**
+   * @description custom class name for Message
+   */
+  customClass: {
+    type: String,
+    default: messageDefaults.customClass,
+  },
+  /**
+   * @description whether to center the text
+   */
+  center: {
+    type: Boolean,
+    default: messageDefaults.center,
+  },
+  /**
+   * @description whether `message` is treated as HTML string
+   */
+  dangerouslyUseHTMLString: {
+    type: Boolean,
+    default: messageDefaults.dangerouslyUseHTMLString,
+  },
+  /**
+   * @description display duration, millisecond. If set to 0, it will not turn off automatically
+   */
+  duration: {
+    type: Number,
+    default: messageDefaults.duration,
+  },
+  /**
+   * @description custom icon component, overrides `type`
+   */
+  icon: {
+    type: iconPropType,
+    default: messageDefaults.icon,
+  },
+  /**
+   * @description message dom id
+   */
+  id: {
+    type: String,
+    default: messageDefaults.id,
+  },
+  /**
+   * @description message text
+   */
+  message: {
+    type: definePropType<string | VNode | (() => VNode)>([
+      String,
+      Object,
+      Function,
+    ]),
+    default: messageDefaults.message,
+  },
+  /**
+   * @description callback function when closed with the message instance as the parameter
+   */
+  onClose: {
+    type: definePropType<() => void>(Function),
+    required: false,
+  },
+  /**
+   * @description whether to show a close button
+   */
+  showClose: {
+    type: Boolean,
+    default: messageDefaults.showClose,
+  },
+  /**
+   * @description message type
+   */
+  type: {
+    type: String,
+    values: messageTypes,
+    default: messageDefaults.type,
+  },
+  /**
+   * @description set the distance to the top of viewport
+   */
+  offset: {
+    type: Number,
+    default: messageDefaults.offset,
+  },
+  /**
+   * @description input box size
+   */
+  zIndex: {
+    type: Number,
+    default: messageDefaults.zIndex,
+  },
+  /**
+   * @description merge messages with the same content, type of VNode message is not supported
+   */
+  grouping: {
+    type: Boolean,
+    default: messageDefaults.grouping,
+  },
+  /**
+   * @description The number of repetitions, similar to badge, is used as the initial number when used with `grouping`
+   */
+  repeatNum: {
+    type: Number,
+    default: messageDefaults.repeatNum,
+  },
+} as const)
+export type MessageProps = ExtractPropTypes<typeof messageProps>
+
+export const messageEmits = {
+  destroy: () => true,
+}
+export type MessageEmits = typeof messageEmits
+
+export type MessageInstance = InstanceType<typeof MessageConstructor>
+
+export type MessageOptions = Partial<
+  Mutable<
+    Omit<MessageProps, 'id'> & {
+      appendTo?: HTMLElement | string
+    }
+  >
+>
+export type MessageParams = MessageOptions | MessageOptions['message']
+export type MessageParamsNormalized = Omit<MessageProps, 'id'> & {
+  /**
+   * @description set the root element for the message, default to `document.body`
+   */
+  appendTo: HTMLElement
+}
+export type MessageOptionsWithType = Omit<MessageOptions, 'type'>
+export type MessageParamsWithType =
+  | MessageOptionsWithType
+  | MessageOptions['message']
+
+export interface MessageHandler {
+  /**
+   * @description close the Message
+   */
+  close: () => void
+}
+
+export type MessageFn = {
+  (options?: MessageParams, appContext?: null | AppContext): MessageHandler
+  closeAll(type?: messageType): void
+}
+export type MessageTypedFn = (
+  options?: MessageParamsWithType,
+  appContext?: null | AppContext
+) => MessageHandler
+
+export interface Message extends MessageFn {
+  success: MessageTypedFn
+  warning: MessageTypedFn
+  info: MessageTypedFn
+  error: MessageTypedFn
+}

+ 145 - 0
packages/components/basic/message/src/message.vue

@@ -0,0 +1,145 @@
+<template>
+  <transition
+    :name="ns.b('fade')"
+    @before-leave="onClose"
+    @after-leave="$emit('destroy')"
+  >
+    <div
+      v-show="visible"
+      :id="id"
+      ref="messageRef"
+      :class="[
+        ns.b(),
+        { [ns.m(type)]: type && !icon },
+        ns.is('center', center),
+        ns.is('closable', showClose),
+        customClass,
+      ]"
+      :style="customStyle"
+      role="alert"
+      @mouseenter="clearTimer"
+      @mouseleave="startTimer"
+    >
+      <el-badge
+        v-if="repeatNum > 1"
+        :value="repeatNum"
+        :type="badgeType"
+        :class="ns.e('badge')"
+      />
+      <kk-icon v-if="iconComponent" :class="[ns.e('icon'), typeClass]">
+        <component :is="iconComponent" />
+      </kk-icon>
+      <slot>
+        <p v-if="!dangerouslyUseHTMLString" :class="ns.e('content')">
+          {{ message }}
+        </p>
+        <!-- Caution here, message could've been compromised, never use user's input as message -->
+        <p v-else :class="ns.e('content')" v-html="message" />
+      </slot>
+      <kk-icon v-if="showClose" :class="ns.e('closeBtn')" @click.stop="close">
+        <Close />
+      </kk-icon>
+    </div>
+  </transition>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, ref, watch } from 'vue'
+import { useEventListener, useResizeObserver, useTimeoutFn } from '@vueuse/core'
+import { TypeComponents, TypeComponentsMap } from '@kankan-components/utils'
+import { EVENT_CODE } from '@kankan-components/constants'
+import ElBadge from '@kankan-components/components/basic/badge'
+import { useGlobalComponentSettings } from '@kankan-components/components/basic/config-provider'
+import { KkIcon } from '@kankan-components/components/basic/icon'
+import { messageEmits, messageProps } from './message'
+import { getLastOffset, getOffsetOrSpace } from './instance'
+import type { BadgeProps } from '@kankan-components/components/basic/badge'
+import type { CSSProperties } from 'vue'
+
+const { Close } = TypeComponents
+
+defineOptions({
+  name: 'ElMessage',
+})
+
+const props = defineProps(messageProps)
+defineEmits(messageEmits)
+
+const { ns, zIndex } = useGlobalComponentSettings('message')
+const { currentZIndex, nextZIndex } = zIndex
+
+const messageRef = ref<HTMLDivElement>()
+const visible = ref(false)
+const height = ref(0)
+
+let stopTimer: (() => void) | undefined = undefined
+
+const badgeType = computed<BadgeProps['type']>(() =>
+  props.type ? (props.type === 'error' ? 'danger' : props.type) : 'info'
+)
+const typeClass = computed(() => {
+  const type = props.type
+  return { [ns.bm('icon', type)]: type && TypeComponentsMap[type] }
+})
+const iconComponent = computed(
+  () => props.icon || TypeComponentsMap[props.type] || ''
+)
+
+const lastOffset = computed(() => getLastOffset(props.id))
+const offset = computed(
+  () => getOffsetOrSpace(props.id, props.offset) + lastOffset.value
+)
+const bottom = computed((): number => height.value + offset.value)
+const customStyle = computed<CSSProperties>(() => ({
+  top: `${offset.value}px`,
+  zIndex: currentZIndex.value,
+}))
+
+function startTimer() {
+  if (props.duration === 0) return
+  ;({ stop: stopTimer } = useTimeoutFn(() => {
+    close()
+  }, props.duration))
+}
+
+function clearTimer() {
+  stopTimer?.()
+}
+
+function close() {
+  visible.value = false
+}
+
+function keydown({ code }: KeyboardEvent) {
+  if (code === EVENT_CODE.esc) {
+    // press esc to close the message
+    close()
+  }
+}
+
+onMounted(() => {
+  startTimer()
+  nextZIndex()
+  visible.value = true
+})
+
+watch(
+  () => props.repeatNum,
+  () => {
+    clearTimer()
+    startTimer()
+  }
+)
+
+useEventListener(document, 'keydown', keydown)
+
+useResizeObserver(messageRef, () => {
+  height.value = messageRef.value!.getBoundingClientRect().height
+})
+
+defineExpose({
+  visible,
+  bottom,
+  close,
+})
+</script>

+ 185 - 0
packages/components/basic/message/src/method.ts

@@ -0,0 +1,185 @@
+import { createVNode, render } from 'vue'
+import {
+  debugWarn,
+  isClient,
+  isElement,
+  isFunction,
+  isNumber,
+  isString,
+  isVNode,
+} from '@kankan-components/utils'
+import { messageConfig } from '@kankan-components/components/basic/config-provider'
+import MessageConstructor from './message.vue'
+import { messageDefaults, messageTypes } from './message'
+import { instances } from './instance'
+
+import type { MessageContext } from './instance'
+import type { AppContext } from 'vue'
+import type {
+  Message,
+  MessageFn,
+  MessageHandler,
+  MessageOptions,
+  MessageParams,
+  MessageParamsNormalized,
+  messageType,
+} from './message'
+
+let seed = 1
+
+// TODO: Since Notify.ts is basically the same like this file. So we could do some encapsulation against them to reduce code duplication.
+
+const normalizeOptions = (params?: MessageParams) => {
+  const options: MessageOptions =
+    !params || isString(params) || isVNode(params) || isFunction(params)
+      ? { message: params }
+      : params
+
+  const normalized = {
+    ...messageDefaults,
+    ...options,
+  }
+
+  if (!normalized.appendTo) {
+    normalized.appendTo = document.body
+  } else if (isString(normalized.appendTo)) {
+    let appendTo = document.querySelector<HTMLElement>(normalized.appendTo)
+
+    // should fallback to default value with a warning
+    if (!isElement(appendTo)) {
+      debugWarn(
+        'ElMessage',
+        'the appendTo option is not an HTMLElement. Falling back to document.body.'
+      )
+      appendTo = document.body
+    }
+
+    normalized.appendTo = appendTo
+  }
+
+  return normalized as MessageParamsNormalized
+}
+
+const closeMessage = (instance: MessageContext) => {
+  const idx = instances.indexOf(instance)
+  if (idx === -1) return
+
+  instances.splice(idx, 1)
+  const { handler } = instance
+  handler.close()
+}
+
+const createMessage = (
+  { appendTo, ...options }: MessageParamsNormalized,
+  context?: AppContext | null
+): MessageContext => {
+  const id = `message_${seed++}`
+  const userOnClose = options.onClose
+
+  const container = document.createElement('div')
+
+  const props = {
+    ...options,
+    // now the zIndex will be used inside the message.vue component instead of here.
+    // zIndex: nextIndex() + options.zIndex
+    id,
+    onClose: () => {
+      userOnClose?.()
+      closeMessage(instance)
+    },
+
+    // clean message element preventing mem leak
+    onDestroy: () => {
+      // since the element is destroy, then the VNode should be collected by GC as well
+      // we do not want cause any mem leak because we have returned vm as a reference to users
+      // so that we manually set it to false.
+      render(null, container)
+    },
+  }
+  const vnode = createVNode(
+    MessageConstructor,
+    props,
+    isFunction(props.message) || isVNode(props.message)
+      ? {
+          default: isFunction(props.message)
+            ? props.message
+            : () => props.message,
+        }
+      : null
+  )
+  vnode.appContext = context || message._context
+
+  render(vnode, container)
+  // instances will remove this item when close function gets called. So we do not need to worry about it.
+  appendTo.appendChild(container.firstElementChild!)
+
+  const vm = vnode.component!
+
+  const handler: MessageHandler = {
+    // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle
+    // for out component, so that all closing steps will not be skipped.
+    close: () => {
+      vm.exposed!.visible.value = false
+    },
+  }
+
+  const instance: MessageContext = {
+    id,
+    vnode,
+    vm,
+    handler,
+    props: (vnode.component as any).props,
+  }
+
+  return instance
+}
+
+const message: MessageFn &
+  Partial<Message> & { _context: AppContext | null } = (
+  options = {},
+  context
+) => {
+  if (!isClient) return { close: () => undefined }
+
+  if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
+    return { close: () => undefined }
+  }
+
+  const normalized = normalizeOptions(options)
+
+  if (normalized.grouping && instances.length) {
+    const instance = instances.find(
+      ({ vnode: vm }) => vm.props?.message === normalized.message
+    )
+    if (instance) {
+      instance.props.repeatNum += 1
+      instance.props.type = normalized.type
+      return instance.handler
+    }
+  }
+
+  const instance = createMessage(normalized, context)
+
+  instances.push(instance)
+  return instance.handler
+}
+
+messageTypes.forEach((type) => {
+  message[type] = (options = {}, appContext) => {
+    const normalized = normalizeOptions(options)
+    return message({ ...normalized, type }, appContext)
+  }
+})
+
+export function closeAll(type?: messageType): void {
+  for (const instance of instances) {
+    if (!type || type === instance.props.type) {
+      instance.handler.close()
+    }
+  }
+}
+
+message.closeAll = closeAll
+message._context = null
+
+export default message as Message

+ 3 - 0
packages/components/basic/message/style/css.ts

@@ -0,0 +1,3 @@
+import '@kankan-components/components/base/style/css'
+import '@kankan-components/components/badge/style/css'
+import '@kankan-components/theme-chalk/el-message.css'

+ 3 - 0
packages/components/basic/message/style/index.ts

@@ -0,0 +1,3 @@
+import '@kankan-components/components/base/style'
+import '@kankan-components/components/badge/style'
+import '@kankan-components/theme-chalk/src/message.scss'

+ 1 - 1
packages/constants/date.ts

@@ -20,4 +20,4 @@ export const WEEK_DAYS = [
   'sat',
 ] as const
 
-export type DatePickType = (typeof datePickTypes)[number]
+export type DatePickType = typeof datePickTypes[number]

+ 1 - 1
packages/constants/size.ts

@@ -1,6 +1,6 @@
 export const componentSizes = ['', 'default', 'small', 'large'] as const
 
-export type ComponentSize = (typeof componentSizes)[number]
+export type ComponentSize = typeof componentSizes[number]
 
 export const componentSizeMap = {
   large: 40,

+ 5 - 2
packages/hooks/index.ts

@@ -1,5 +1,4 @@
 export * from './use-z-index'
-export * from './use-global-config'
 export * from './use-namespace'
 export * from './use-same-target'
 export * from './use-id'
@@ -7,7 +6,11 @@ export * from './use-draggable'
 export * from './use-escape-keydown'
 export * from './use-lockscreen'
 export * from './use-escape-keydown'
-export * from './use-common-props'
 export * from './use-attrs'
 export * from './use-cursor'
 export * from './use-restore-active'
+export * from './use-size'
+export * from './use-prop'
+export * from './use-locale'
+export * from './use-focus'
+export * from './use-focus-controller'

+ 0 - 46
packages/hooks/use-common-props/index.ts

@@ -1,46 +0,0 @@
-import { computed, ref, unref } from 'vue'
-// import { formContextKey, formItemContextKey } from '@kankan-components/tokens'
-import { buildProp } from '@kankan-components/utils'
-import { componentSizes } from '@kankan-components/constants'
-import { useProp } from '../use-prop'
-import { useGlobalConfig } from '../use-global-config'
-import type { ComponentSize } from '@kankan-components/constants'
-import type { MaybeRef } from '@vueuse/core'
-
-export const useSizeProp = buildProp({
-  type: String,
-  values: componentSizes,
-  required: false,
-} as const)
-
-export const useSize = (
-  fallback?: MaybeRef<ComponentSize | undefined>,
-  ignore: Partial<Record<'prop' | 'form' | 'formItem' | 'global', boolean>> = {}
-) => {
-  const emptyRef = ref(undefined)
-
-  const size = ignore.prop ? emptyRef : useProp<ComponentSize>('size')
-  const globalConfig = ignore.global ? emptyRef : useGlobalConfig('size')
-  // const form = ignore.form
-  //   ? { size: undefined }
-  //   : inject(formContextKey, undefined)
-  // const formItem = ignore.formItem
-  //   ? { size: undefined }
-  //   : inject(formItemContextKey, undefined)
-
-  return computed(
-    (): ComponentSize =>
-      size.value ||
-      unref(fallback) ||
-      // formItem?.size ||
-      // form?.size ||
-      globalConfig.value ||
-      ''
-  )
-}
-
-export const useDisabled = (fallback?: MaybeRef<boolean | undefined>) => {
-  const disabled = useProp<boolean>('disabled')
-  // const form = inject(formContextKey, undefined)
-  return computed(() => disabled.value || unref(fallback) || false)
-}

+ 59 - 0
packages/hooks/use-focus-controller/index.ts

@@ -0,0 +1,59 @@
+import { getCurrentInstance, ref, shallowRef, watch } from 'vue'
+import { useEventListener } from '@vueuse/core'
+import type { ShallowRef } from 'vue'
+
+interface UseFocusControllerOptions {
+  afterFocus?: () => void
+  afterBlur?: () => void
+}
+
+export function useFocusController<T extends HTMLElement>(
+  target: ShallowRef<T | undefined>,
+  { afterFocus, afterBlur }: UseFocusControllerOptions = {}
+) {
+  const instance = getCurrentInstance()!
+  const { emit } = instance
+  const wrapperRef = shallowRef<HTMLElement>()
+  const isFocused = ref(false)
+
+  const handleFocus = (event: FocusEvent) => {
+    if (isFocused.value) return
+    isFocused.value = true
+    emit('focus', event)
+    afterFocus?.()
+  }
+
+  const handleBlur = (event: FocusEvent) => {
+    if (
+      event.relatedTarget &&
+      wrapperRef.value?.contains(event.relatedTarget as Node)
+    )
+      return
+
+    isFocused.value = false
+    emit('blur', event)
+    afterBlur?.()
+  }
+
+  const handleClick = () => {
+    target.value?.focus()
+  }
+
+  watch(wrapperRef, (el) => {
+    if (el) {
+      el.setAttribute('tabindex', '-1')
+    }
+  })
+
+  // TODO: using useEventListener will fail the test
+  // useEventListener(target, 'focus', handleFocus)
+  // useEventListener(target, 'blur', handleBlur)
+  useEventListener(wrapperRef, 'click', handleClick)
+
+  return {
+    wrapperRef,
+    isFocused,
+    handleFocus,
+    handleBlur,
+  }
+}

+ 13 - 0
packages/hooks/use-focus/index.ts

@@ -0,0 +1,13 @@
+import type { Ref } from 'vue'
+
+export const useFocus = (
+  el: Ref<{
+    focus: () => void
+  } | null>
+) => {
+  return {
+    focus: () => {
+      el.value?.focus?.()
+    },
+  }
+}

+ 6 - 8
packages/hooks/use-id/index.ts

@@ -1,13 +1,11 @@
 import { computed, getCurrentInstance, inject, unref } from 'vue'
-import { isClient } from '@vueuse/core'
-import { debugWarn } from '@kankan-components/utils'
-import { useGlobalConfig } from '../use-global-config'
-import { defaultNamespace } from '../use-namespace'
+import { debugWarn, isClient } from '@kankan-components/utils'
+import { useGetDerivedNamespace } from '../use-namespace'
 
 import type { InjectionKey, Ref } from 'vue'
 import type { MaybeRef } from '@vueuse/core'
 
-export type UiIdInjectionContext = {
+export type ElIdInjectionContext = {
   prefix: number
   current: number
 }
@@ -17,10 +15,10 @@ const defaultIdInjection = {
   current: 0,
 }
 
-export const ID_INJECTION_KEY: InjectionKey<UiIdInjectionContext> =
+export const ID_INJECTION_KEY: InjectionKey<ElIdInjectionContext> =
   Symbol('elIdInjection')
 
-export const useIdInjection = (): UiIdInjectionContext => {
+export const useIdInjection = (): ElIdInjectionContext => {
   return getCurrentInstance()
     ? inject(ID_INJECTION_KEY, defaultIdInjection)
     : defaultIdInjection
@@ -39,7 +37,7 @@ usage: app.provide(ID_INJECTION_KEY, {
     )
   }
 
-  const namespace = useGlobalConfig('namespace', defaultNamespace)
+  const namespace = useGetDerivedNamespace()
   const idRef = computed(
     () =>
       unref(deterministicId) ||

+ 50 - 0
packages/hooks/use-locale/index.ts

@@ -0,0 +1,50 @@
+import { computed, inject, isRef, ref, unref } from 'vue'
+import { get } from 'lodash-unified'
+import English from '@kankan-components/locale/lang/en'
+
+import type { MaybeRef } from '@vueuse/core'
+import type { InjectionKey, Ref } from 'vue'
+import type { Language } from '@kankan-components/locale'
+
+export type TranslatorOption = Record<string, string | number>
+export type Translator = (path: string, option?: TranslatorOption) => string
+export type LocaleContext = {
+  locale: Ref<Language>
+  lang: Ref<string>
+  t: Translator
+}
+
+export const buildTranslator =
+  (locale: MaybeRef<Language>): Translator =>
+  (path, option) =>
+    translate(path, option, unref(locale))
+
+export const translate = (
+  path: string,
+  option: undefined | TranslatorOption,
+  locale: Language
+): string =>
+  (get(locale, path, path) as string).replace(
+    /\{(\w+)\}/g,
+    (_, key) => `${option?.[key] ?? `{${key}}`}`
+  )
+
+export const buildLocaleContext = (
+  locale: MaybeRef<Language>
+): LocaleContext => {
+  const lang = computed(() => unref(locale).name)
+  const localeRef = isRef(locale) ? locale : ref(locale)
+  return {
+    lang,
+    locale: localeRef,
+    t: buildTranslator(locale),
+  }
+}
+
+export const localeContextKey: InjectionKey<Ref<Language | undefined>> =
+  Symbol('localeContextKey')
+
+export const useLocale = (localeOverrides?: Ref<Language | undefined>) => {
+  const locale = localeOverrides || inject(localeContextKey, ref())!
+  return buildLocaleContext(computed(() => locale.value || English))
+}

+ 26 - 4
packages/hooks/use-namespace/index.ts

@@ -1,6 +1,8 @@
-import { useGlobalConfig } from '../use-global-config'
+import { computed, getCurrentInstance, inject, ref, unref } from 'vue'
 
-export const defaultNamespace = 'kk'
+import type { InjectionKey, Ref } from 'vue'
+
+export const defaultNamespace = 'el'
 const statePrefix = 'is-'
 
 const _bem = (
@@ -23,8 +25,28 @@ const _bem = (
   return cls
 }
 
-export const useNamespace = (block: string) => {
-  const namespace = useGlobalConfig('namespace', defaultNamespace)
+export const namespaceContextKey: InjectionKey<Ref<string | undefined>> =
+  Symbol('namespaceContextKey')
+
+export const useGetDerivedNamespace = (
+  namespaceOverrides?: Ref<string | undefined>
+) => {
+  const derivedNamespace =
+    namespaceOverrides ||
+    (getCurrentInstance()
+      ? inject(namespaceContextKey, ref(defaultNamespace))
+      : ref(defaultNamespace))
+  const namespace = computed(() => {
+    return unref(derivedNamespace) || defaultNamespace
+  })
+  return namespace
+}
+
+export const useNamespace = (
+  block: string,
+  namespaceOverrides?: Ref<string | undefined>
+) => {
+  const namespace = useGetDerivedNamespace(namespaceOverrides)
   const b = (blockSuffix = '') =>
     _bem(namespace.value, block, blockSuffix, '', '')
   const e = (element?: string) =>

+ 30 - 0
packages/hooks/use-size/index.ts

@@ -0,0 +1,30 @@
+import { computed, inject, unref } from 'vue'
+import { buildProp } from '@kankan-components/utils'
+import { componentSizes } from '@kankan-components/constants'
+
+import type { InjectionKey, Ref } from 'vue'
+import type { ComponentSize } from '@kankan-components/constants'
+
+export const useSizeProp = buildProp({
+  type: String,
+  values: componentSizes,
+  required: false,
+} as const)
+
+export const useSizeProps = {
+  size: useSizeProp,
+}
+
+export interface SizeContext {
+  size: Ref<ComponentSize>
+}
+
+export const SIZE_INJECTION_KEY: InjectionKey<SizeContext> = Symbol('size')
+
+export const useGlobalSize = () => {
+  const injectedSize = inject(SIZE_INJECTION_KEY, {} as SizeContext)
+
+  return computed<ComponentSize>(() => {
+    return unref(injectedSize.size) || ''
+  })
+}

+ 20 - 5
packages/hooks/use-z-index/index.ts

@@ -1,11 +1,24 @@
-import { computed, ref } from 'vue'
-// import { useGlobalConfig } from '../use-global-config'
+import { computed, getCurrentInstance, inject, ref, unref } from 'vue'
+import { isNumber } from '@kankan-components/utils'
+
+import type { InjectionKey, Ref } from 'vue'
 
 const zIndex = ref(0)
+export const defaultInitialZIndex = 2000
+
+export const zIndexContextKey: InjectionKey<Ref<number | undefined>> =
+  Symbol('zIndexContextKey')
 
-export const useZIndex = () => {
-  // const initialZIndex = useGlobalConfig('zIndex', 2000) // TODO: move to @element-plus/constants
-  const initialZIndex = ref(1000)
+export const useZIndex = (zIndexOverrides?: Ref<number>) => {
+  const zIndexInjection =
+    zIndexOverrides ||
+    (getCurrentInstance() ? inject(zIndexContextKey, undefined) : undefined)
+  const initialZIndex = computed(() => {
+    const zIndexFromInjection = unref(zIndexInjection)
+    return isNumber(zIndexFromInjection)
+      ? zIndexFromInjection
+      : defaultInitialZIndex
+  })
   const currentZIndex = computed(() => initialZIndex.value + zIndex.value)
 
   const nextZIndex = () => {
@@ -19,3 +32,5 @@ export const useZIndex = () => {
     nextZIndex,
   }
 }
+
+export type UseZIndexReturn = ReturnType<typeof useZIndex>

+ 0 - 1
packages/kankan-components/index.ts

@@ -3,7 +3,6 @@ export * from '@kankan-components/components'
 export * from '@kankan-components/constants'
 export * from '@kankan-components/directives'
 export * from '@kankan-components/hooks'
-export * from '@kankan-components/tokens'
 export * from './make-installer'
 
 export const install = installer.install

+ 1 - 0
packages/kankan-components/locales.ts

@@ -0,0 +1 @@
+export * from '@kankan-components/locale'

+ 2 - 2
packages/kankan-components/make-installer.ts

@@ -1,9 +1,9 @@
-import { provideGlobalConfig } from '@kankan-components/hooks'
+import { provideGlobalConfig } from '@kankan-components/components/basic/config-provider'
 import { INSTALLED_KEY } from '@kankan-components/constants'
 import { version } from './version'
 
 import type { App, Plugin } from '@vue/runtime-core'
-import type { ConfigProviderContext } from '@kankan-components/tokens'
+import type { ConfigProviderContext } from '@kankan-components/components/basic/config-provider'
 
 export const makeInstaller = (components: Plugin[] = []) => {
   const install = (app: App, options?: ConfigProviderContext) => {

+ 64 - 0
packages/locale/index.ts

@@ -0,0 +1,64 @@
+export { default as af } from './lang/af'
+export { default as ar } from './lang/ar'
+export { default as az } from './lang/az'
+export { default as bg } from './lang/bg'
+export { default as bn } from './lang/bn'
+export { default as ca } from './lang/ca'
+export { default as cs } from './lang/cs'
+export { default as da } from './lang/da'
+export { default as de } from './lang/de'
+export { default as el } from './lang/el'
+export { default as en } from './lang/en'
+export { default as eo } from './lang/eo'
+export { default as es } from './lang/es'
+export { default as et } from './lang/et'
+export { default as eu } from './lang/eu'
+export { default as fa } from './lang/fa'
+export { default as fi } from './lang/fi'
+export { default as fr } from './lang/fr'
+export { default as he } from './lang/he'
+export { default as hr } from './lang/hr'
+export { default as hu } from './lang/hu'
+export { default as hyAm } from './lang/hy-am'
+export { default as id } from './lang/id'
+export { default as it } from './lang/it'
+export { default as ja } from './lang/ja'
+export { default as kk } from './lang/kk'
+export { default as km } from './lang/km'
+export { default as ko } from './lang/ko'
+export { default as ku } from './lang/ku'
+export { default as ky } from './lang/ky'
+export { default as lt } from './lang/lt'
+export { default as lv } from './lang/lv'
+export { default as mn } from './lang/mn'
+export { default as nbNo } from './lang/nb-no'
+export { default as nl } from './lang/nl'
+export { default as pa } from './lang/pa'
+export { default as pl } from './lang/pl'
+export { default as ptBr } from './lang/pt-br'
+export { default as pt } from './lang/pt'
+export { default as ro } from './lang/ro'
+export { default as ru } from './lang/ru'
+export { default as sk } from './lang/sk'
+export { default as sl } from './lang/sl'
+export { default as sr } from './lang/sr'
+export { default as sv } from './lang/sv'
+export { default as ta } from './lang/ta'
+export { default as th } from './lang/th'
+export { default as tk } from './lang/tk'
+export { default as tr } from './lang/tr'
+export { default as ugCn } from './lang/ug-cn'
+export { default as uk } from './lang/uk'
+export { default as uzUz } from './lang/uz-uz'
+export { default as vi } from './lang/vi'
+export { default as zhCn } from './lang/zh-cn'
+export { default as zhTw } from './lang/zh-tw'
+
+export type TranslatePair = {
+  [key: string]: string | string[] | TranslatePair
+}
+
+export type Language = {
+  name: string
+  el: TranslatePair
+}

+ 127 - 0
packages/locale/lang/af.ts

@@ -0,0 +1,127 @@
+export default {
+  name: 'af',
+  el: {
+    colorpicker: {
+      confirm: 'Bevestig',
+      clear: 'Maak skoon',
+    },
+    datepicker: {
+      now: 'Nou',
+      today: 'Vandag',
+      cancel: 'Kanselleer',
+      clear: 'Maak skoon',
+      confirm: 'Bevestig',
+      selectDate: 'Kies datum',
+      selectTime: 'Kies tyd',
+      startDate: 'Begindatum',
+      startTime: 'Begintyd',
+      endDate: 'Einddatum',
+      endTime: 'Eindtyd',
+      prevYear: 'Previous Year', // to be translated
+      nextYear: 'Next Year', // to be translated
+      prevMonth: 'Previous Month', // to be translated
+      nextMonth: 'Next Month', // to be translated
+      year: 'Jaar',
+      month1: 'Jan',
+      month2: 'Feb',
+      month3: 'Mrt',
+      month4: 'Apr',
+      month5: 'Mei',
+      month6: 'Jun',
+      month7: 'Jul',
+      month8: 'Aug',
+      month9: 'Sep',
+      month10: 'Okt',
+      month11: 'Nov',
+      month12: 'Des',
+      // week: 'week',
+      weeks: {
+        sun: 'So',
+        mon: 'Ma',
+        tue: 'Di',
+        wed: 'Wo',
+        thu: 'Do',
+        fri: 'Vr',
+        sat: 'Sa',
+      },
+      months: {
+        jan: 'Jan',
+        feb: 'Feb',
+        mar: 'Mrt',
+        apr: 'Apr',
+        may: 'Mei',
+        jun: 'Jun',
+        jul: 'Jul',
+        aug: 'Aug',
+        sep: 'Sep',
+        oct: 'Okt',
+        nov: 'Nov',
+        dec: 'Des',
+      },
+    },
+    select: {
+      loading: 'Laai',
+      noMatch: 'Geen toepaslike data',
+      noData: 'Geen data',
+      placeholder: 'Kies',
+    },
+    cascader: {
+      noMatch: 'Geen toepaslike data',
+      loading: 'Laai',
+      placeholder: 'Kies',
+      noData: 'Geen data',
+    },
+    pagination: {
+      goto: 'Gaan na',
+      pagesize: '/page',
+      total: 'Totaal {total}',
+      pageClassifier: '',
+      page: 'Page', // to be translated
+      prev: 'Go to previous page', // to be translated
+      next: 'Go to next page', // to be translated
+      currentPage: 'page {pager}', // to be translated
+      prevPages: 'Previous {pager} pages', // to be translated
+      nextPages: 'Next {pager} pages', // to be translated
+    },
+    messagebox: {
+      title: 'Boodskap',
+      confirm: 'Bevestig',
+      cancel: 'Kanselleer',
+      error: 'Ongeldige invoer',
+    },
+    upload: {
+      deleteTip: 'press delete to remove', // to be translated
+      delete: 'Verwyder',
+      preview: 'Voorskou',
+      continue: 'Gaan voort',
+    },
+    table: {
+      emptyText: 'Geen Data',
+      confirmFilter: 'Bevestig',
+      resetFilter: 'Herstel',
+      clearFilter: 'Alles',
+      sumText: 'Som',
+    },
+    tree: {
+      emptyText: 'Geen Data',
+    },
+    transfer: {
+      noMatch: 'Geen toepaslike data',
+      noData: 'Geen data',
+      titles: ['Lys 1', 'Lys 2'],
+      filterPlaceholder: 'Voer sleutelwoord in',
+      noCheckedFormat: '{total} items',
+      hasCheckedFormat: '{checked}/{total} gekies',
+    },
+    image: {
+      error: 'FAILED', // to be translated
+    },
+    pageHeader: {
+      title: 'Back', // to be translated
+    },
+    popconfirm: {
+      confirmButtonText: 'Yes', // to be translated
+      cancelButtonText: 'No', // to be translated
+    },
+  },
+}

+ 147 - 0
packages/locale/lang/ar.ts

@@ -0,0 +1,147 @@
+export default {
+  name: 'ar',
+  el: {
+    colorpicker: {
+      confirm: 'موافق',
+      clear: 'إزالة',
+      defaultLabel: 'إختر اللون',
+      description: 'اللون الحالي هو {color}. اضفط انتر لاختيار لون جديد',
+    },
+    datepicker: {
+      now: 'الآن',
+      today: 'اليوم',
+      cancel: 'إلغاء',
+      clear: 'إزالة',
+      confirm: 'موافق',
+      dateTablePrompt:
+        'استخدم مفاتيح الاسهم و اضغط انتر لاختيار اليوم المراد من الشهر',
+      monthTablePrompt: 'استخدم مفاتيح الاسهم واضغط انتر لاختيار الشهر',
+      yearTablePrompt: 'استخدم مفاتيح الاسهم واضغط انتر لاختيار السنة',
+      selectDate: 'إختر التاريخ',
+      selectTime: 'إختر الوقت',
+      startDate: 'تاريخ البدء',
+      startTime: 'وقت البدء',
+      endDate: 'تاريخ الإنتهاء',
+      endTime: 'وقت الإنتهاء',
+      prevYear: 'السنة السابقة',
+      nextYear: 'السنة التالية',
+      prevMonth: 'الشهر السابق',
+      nextMonth: 'الشهر التالي',
+      year: 'سنة',
+      month1: 'كانون الثاني',
+      month2: 'شباط',
+      month3: 'اذار',
+      month4: 'نيسان',
+      month5: 'أيار',
+      month6: 'حزيران',
+      month7: 'تموز',
+      month8: 'اّب',
+      month9: 'ايلول',
+      month10: 'تشرين الاول',
+      month11: 'تشرين الثاني',
+      month12: 'كانون الاول',
+      week: 'أسبوع',
+      weeks: {
+        sun: 'الأحد',
+        mon: 'الأثنين',
+        tue: 'الثلاثاء',
+        wed: 'الأربعاء',
+        thu: 'الخميس',
+        fri: 'الجمعة',
+        sat: 'السبت',
+      },
+      months: {
+        jan: 'كانون الثاني',
+        feb: 'شباط',
+        mar: 'اذار',
+        apr: 'نيسان',
+        may: 'ايار',
+        jun: 'حزيران',
+        jul: 'تمور',
+        aug: 'اّب',
+        sep: 'ايلول',
+        oct: 'تشرين الاول',
+        nov: 'تشرين الثاني',
+        dec: 'كانون الاول',
+      },
+    },
+    inputNumber: {
+      decrease: 'طرح رقم',
+      increase: 'زيادة رقم',
+    },
+    select: {
+      loading: 'جار التحميل',
+      noMatch: 'لايوجد بيانات مطابقة',
+      noData: 'لايوجد بيانات',
+      placeholder: 'إختر',
+    },
+    dropdown: {
+      toggleDropdown: 'تبديل القائمة',
+    },
+    cascader: {
+      noMatch: 'لايوجد بيانات مطابقة',
+      loading: 'جار التحميل',
+      placeholder: 'إختر',
+      noData: 'لايوجد بيانات',
+    },
+    pagination: {
+      goto: 'أذهب إلى',
+      pagesize: '/صفحة',
+      total: 'الكل {total}',
+      pageClassifier: '',
+      page: 'Page', // to be translated
+      prev: 'Go to previous page', // to be translated
+      next: 'Go to next page', // to be translated
+      currentPage: 'page {pager}', // to be translated
+      prevPages: 'Previous {pager} pages', // to be translated
+      nextPages: 'Next {pager} pages', // to be translated
+    },
+    dialog: {
+      close: 'أغلق هذا التبويب',
+    },
+    drawer: {
+      close: 'أغلق هذا التبويب',
+    },
+    messagebox: {
+      title: 'العنوان',
+      confirm: 'موافق',
+      cancel: 'إلغاء',
+      error: 'مدخل غير صحيح',
+      close: 'أغلق هذا التبويب',
+    },
+    upload: {
+      deleteTip: 'اضغط ازالة لحذف المحتوى',
+      delete: 'حذف',
+      preview: 'عرض',
+      continue: 'إستمرار',
+    },
+    table: {
+      emptyText: 'لايوجد بيانات',
+      confirmFilter: 'تأكيد',
+      resetFilter: 'حذف',
+      clearFilter: 'الكل',
+      sumText: 'المجموع',
+    },
+    tree: {
+      emptyText: 'لايوجد بيانات',
+    },
+    transfer: {
+      noMatch: 'لايوجد بيانات مطابقة',
+      noData: 'لايوجد بيانات',
+      titles: ['قائمة 1', 'قائمة 2'],
+      filterPlaceholder: 'ادخل كلمة',
+      noCheckedFormat: '{total} عناصر',
+      hasCheckedFormat: '{checked}/{total} مختار',
+    },
+    image: {
+      error: 'فشل',
+    },
+    pageHeader: {
+      title: 'عودة',
+    },
+    popconfirm: {
+      confirmButtonText: 'Yes', // to be translated
+      cancelButtonText: 'No', // to be translated
+    },
+  },
+}

+ 130 - 0
packages/locale/lang/az.ts

@@ -0,0 +1,130 @@
+export default {
+  name: 'az',
+  el: {
+    colorpicker: {
+      confirm: 'Təsdiqlə',
+      clear: 'Təmizlə',
+    },
+    datepicker: {
+      now: 'İndi',
+      today: 'Bugün',
+      cancel: 'İmtina',
+      clear: 'Təmizlə',
+      confirm: 'Təsdiqlə',
+      selectDate: 'Tarix seç',
+      selectTime: 'Saat seç',
+      startDate: 'Başlanğıc Tarixi',
+      startTime: 'Başlanğıc Saatı',
+      endDate: 'Bitmə Tarixi',
+      endTime: 'Bitmə Saatı',
+      prevYear: 'Öncəki il',
+      nextYear: 'Sonrakı il',
+      prevMonth: 'Öncəki ay',
+      nextMonth: 'Sonrakı ay',
+      year: '',
+      month1: 'Yanvar',
+      month2: 'Fevral',
+      month3: 'Mart',
+      month4: 'Aprel',
+      month5: 'May',
+      month6: 'İyun',
+      month7: 'İyul',
+      month8: 'Avqust',
+      month9: 'Sentyabr',
+      month10: 'Oktyabr',
+      month11: 'Noyabr',
+      month12: 'Dekabr',
+      week: 'həftə',
+      weeks: {
+        sun: 'Baz',
+        mon: 'B.e',
+        tue: 'Ç.a',
+        wed: 'Çər',
+        thu: 'C.a',
+        fri: 'Cüm',
+        sat: 'Şən',
+      },
+      months: {
+        jan: 'Yan',
+        feb: 'Fev',
+        mar: 'Mar',
+        apr: 'Apr',
+        may: 'May',
+        jun: 'İyn',
+        jul: 'İyl',
+        aug: 'Avq',
+        sep: 'Sen',
+        oct: 'Okt',
+        nov: 'Noy',
+        dec: 'Dek',
+      },
+    },
+    select: {
+      loading: 'Yüklənir',
+      noMatch: 'Nəticə tapılmadı',
+      noData: 'Məlumat yoxdur',
+      placeholder: 'Seç',
+    },
+    cascader: {
+      noMatch: 'Nəticə tapılmadı',
+      loading: 'Yüklənir',
+      placeholder: 'Seç',
+      noData: 'Məlumat yoxdur',
+    },
+    pagination: {
+      goto: 'Get',
+      pagesize: '/səhifə',
+      total: 'Toplam {total}',
+      pageClassifier: '',
+      page: 'Page', // to be translated
+      prev: 'Go to previous page', // to be translated
+      next: 'Go to next page', // to be translated
+      currentPage: 'page {pager}', // to be translated
+      prevPages: 'Previous {pager} pages', // to be translated
+      nextPages: 'Next {pager} pages', // to be translated
+    },
+    messagebox: {
+      title: 'Mesaj',
+      confirm: 'Təsdiqlə',
+      cancel: 'İmtina',
+      error: 'Səhv',
+    },
+    upload: {
+      deleteTip: 'Sürüşdürmədən sonra sil',
+      delete: 'Sil',
+      preview: 'Ön izlə',
+      continue: 'Davam et',
+    },
+    table: {
+      emptyText: 'Məlumat yoxdur',
+      confirmFilter: 'Təsdiqlə',
+      resetFilter: 'Sıfırla',
+      clearFilter: 'Bütün',
+      sumText: 'Cəmi',
+    },
+    tree: {
+      emptyText: 'Məlumat yoxdur',
+    },
+    transfer: {
+      noMatch: 'Nəticə tapılmadı',
+      noData: 'Məlumat yoxdur',
+      titles: ['Siyahı 1', 'Siyahı 2'],
+      filterPlaceholder: 'Kəlimələri daxil et',
+      noCheckedFormat: '{total} ədəd',
+      hasCheckedFormat: '{checked}/{total} seçildi',
+    },
+    image: {
+      error: 'SƏHV', // to be translated
+    },
+    pageHeader: {
+      title: 'Geri', // to be translated
+    },
+    popconfirm: {
+      confirmButtonText: 'Bəli', // to be translated
+      cancelButtonText: 'Xeyr', // to be translated
+    },
+    empty: {
+      description: 'Məlumat yoxdur',
+    },
+  },
+}

+ 127 - 0
packages/locale/lang/bg.ts

@@ -0,0 +1,127 @@
+export default {
+  name: 'bg',
+  el: {
+    colorpicker: {
+      confirm: 'OK',
+      clear: 'Изчисти',
+    },
+    datepicker: {
+      now: 'Сега',
+      today: 'Днес',
+      cancel: 'Откажи',
+      clear: 'Изчисти',
+      confirm: 'ОК',
+      selectDate: 'Избери дата',
+      selectTime: 'Избери час',
+      startDate: 'Начална дата',
+      startTime: 'Начален час',
+      endDate: 'Крайна дата',
+      endTime: 'Краен час',
+      prevYear: 'Previous Year', // to be translated
+      nextYear: 'Next Year', // to be translated
+      prevMonth: 'Previous Month', // to be translated
+      nextMonth: 'Next Month', // to be translated
+      year: '',
+      month1: 'Януари',
+      month2: 'Февруари',
+      month3: 'Март',
+      month4: 'Април',
+      month5: 'Май',
+      month6: 'Юни',
+      month7: 'Юли',
+      month8: 'Август',
+      month9: 'Септември',
+      month10: 'Октомври',
+      month11: 'Ноември',
+      month12: 'Декември',
+      // week: 'Седмица',
+      weeks: {
+        sun: 'Нед',
+        mon: 'Пон',
+        tue: 'Вто',
+        wed: 'Сря',
+        thu: 'Чет',
+        fri: 'Пет',
+        sat: 'Съб',
+      },
+      months: {
+        jan: 'Яну',
+        feb: 'Фев',
+        mar: 'Мар',
+        apr: 'Апр',
+        may: 'Май',
+        jun: 'Юни',
+        jul: 'Юли',
+        aug: 'Авг',
+        sep: 'Сеп',
+        oct: 'Окт',
+        nov: 'Ное',
+        dec: 'Дек',
+      },
+    },
+    select: {
+      loading: 'Зареждане',
+      noMatch: 'Няма намерени',
+      noData: 'Няма данни',
+      placeholder: 'Избери',
+    },
+    cascader: {
+      noMatch: 'Няма намерени',
+      loading: 'Зареждане',
+      placeholder: 'Избери',
+      noData: 'Няма данни',
+    },
+    pagination: {
+      goto: 'Иди на',
+      pagesize: '/страница',
+      total: 'Общо {total}',
+      pageClassifier: '',
+      page: 'Page', // to be translated
+      prev: 'Go to previous page', // to be translated
+      next: 'Go to next page', // to be translated
+      currentPage: 'page {pager}', // to be translated
+      prevPages: 'Previous {pager} pages', // to be translated
+      nextPages: 'Next {pager} pages', // to be translated
+    },
+    messagebox: {
+      title: 'Съобщение',
+      confirm: 'ОК',
+      cancel: 'Откажи',
+      error: 'Невалидни данни',
+    },
+    upload: {
+      deleteTip: 'press delete to remove', // to be translated
+      delete: 'Изтрий',
+      preview: 'Прегледай',
+      continue: 'Продължи',
+    },
+    table: {
+      emptyText: 'Няма данни',
+      confirmFilter: 'Потвърди',
+      resetFilter: 'Изчисти',
+      clearFilter: 'Всички',
+      sumText: 'Sum', // to be translated
+    },
+    tree: {
+      emptyText: 'Няма данни',
+    },
+    transfer: {
+      noMatch: 'Няма намерени',
+      noData: 'Няма данни',
+      titles: ['List 1', 'List 2'], // to be translated
+      filterPlaceholder: 'Enter keyword', // to be translated
+      noCheckedFormat: '{total} items', // to be translated
+      hasCheckedFormat: '{checked}/{total} checked', // to be translated
+    },
+    image: {
+      error: 'FAILED', // to be translated
+    },
+    pageHeader: {
+      title: 'Back', // to be translated
+    },
+    popconfirm: {
+      confirmButtonText: 'Yes', // to be translated
+      cancelButtonText: 'No', // to be translated
+    },
+  },
+}

+ 129 - 0
packages/locale/lang/bn.ts

@@ -0,0 +1,129 @@
+export default {
+  name: 'bn',
+  el: {
+    colorpicker: {
+      confirm: 'ঠিক আছে',
+      clear: 'ক্লিয়ার',
+    },
+    datepicker: {
+      now: 'এখন',
+      today: 'আজ',
+      cancel: 'বাতিল',
+      clear: 'ক্লিয়ার',
+      confirm: 'ঠিক আছে',
+      selectDate: 'তারিখ নির্বাচন করুন',
+      selectTime: 'সময় নির্বাচন করুন',
+      startDate: 'যে তারিখ থেকে',
+      startTime: 'যে সময় থেকে',
+      endDate: 'যে তারিখ পর্যন্ত',
+      endTime: 'যে সময় পর্যন্ত',
+      prevYear: 'পূর্ববর্তী বছর',
+      nextYear: 'পরবর্তী বছর',
+      prevMonth: 'পূর্ববর্তী মাস',
+      nextMonth: 'পরবর্তী মাস',
+      year: 'সাল',
+      month1: 'জানুয়ারি',
+      month2: 'ফেব্রুয়ারী',
+      month3: 'মার্চ',
+      month4: 'এপ্রিল',
+      month5: 'মে',
+      month6: 'জুন',
+      month7: 'জুলাই',
+      month8: 'আগষ্ট',
+      month9: 'সেপ্টেম্বর',
+      month10: 'অক্টোবর',
+      month11: 'নভেম্বর',
+      month12: 'ডিসেম্বর',
+      week: 'সাপ্তাহ',
+      weeks: {
+        sun: 'রবি',
+        mon: 'সোম',
+        tue: 'মঙ্গল',
+        wed: 'বুধ',
+        thu: 'বৃহঃ',
+        fri: 'শুক্র',
+        sat: 'শনি',
+      },
+      months: {
+        jan: 'জানু',
+        feb: 'ফেব্রু',
+        mar: 'মার্চ',
+        apr: 'এপ্রি',
+        may: 'মে',
+        jun: 'জুন',
+        jul: 'জুলা',
+        aug: 'আগ',
+        sep: 'সেপ্টে',
+        oct: 'আক্টো',
+        nov: 'নভে',
+        dec: 'ডিসে',
+      },
+    },
+    select: {
+      loading: 'লোড হচ্ছে',
+      noMatch: 'কোন মিল পওয়া যায়নি',
+      noData: 'কোন ডাটা নেই',
+      placeholder: 'নির্বাচন করুন',
+    },
+    cascader: {
+      noMatch: 'কোন মিল পওয়া যায়নি',
+      loading: 'লোড হচ্ছে',
+      placeholder: 'নির্বাচন করুন',
+      noData: 'কোন ডাটা নেই',
+    },
+    pagination: {
+      goto: 'যান',
+      pagesize: '/পেজ',
+      total: 'মোট {total}',
+      pageClassifier: '',
+      page: 'Page', // to be translated
+      prev: 'Go to previous page', // to be translated
+      next: 'Go to next page', // to be translated
+      currentPage: 'page {pager}', // to be translated
+      prevPages: 'Previous {pager} pages', // to be translated
+      nextPages: 'Next {pager} pages', // to be translated
+      deprecationWarning:
+        'অপ্রচলিত (Deprecated) ব্যাবহার পওয়া গেছে, আরও জানতে চাইলে, দয়া করে el-pagination এর ডকুমেন্টেশন দেখুন',
+    },
+    messagebox: {
+      title: 'বার্তা',
+      confirm: 'ঠিক আছে',
+      cancel: 'বাতিল',
+      error: 'ইনপুট ডাটা গ্রহনযোগ্য নয়',
+    },
+    upload: {
+      deleteTip: 'অপসারণ করতে "ডিলিট" এ ক্লিক করুন',
+      delete: 'ডিলিট',
+      preview: 'প্রিভিউ',
+      continue: 'চালিয়ে যান',
+    },
+    table: {
+      emptyText: 'কোন ডাটা নেই',
+      confirmFilter: 'নিশ্চিত করুন',
+      resetFilter: 'রিসেট',
+      clearFilter: 'সব',
+      sumText: 'সারাংশ',
+    },
+    tree: {
+      emptyText: 'কোন ডাটা নেই',
+    },
+    transfer: {
+      noMatch: 'কোন মিল পওয়া যায়নি',
+      noData: 'কোন ডাটা নেই',
+      titles: ['লিস্ট ১', 'লিস্ট ২'],
+      filterPlaceholder: 'সার্চ করুন',
+      noCheckedFormat: '{total} আইটেম',
+      hasCheckedFormat: '{checked}/{total} টিক করা হয়েছে',
+    },
+    image: {
+      error: 'ব্যর্থ হয়েছে',
+    },
+    pageHeader: {
+      title: 'পিছনে',
+    },
+    popconfirm: {
+      confirmButtonText: 'হ্যা',
+      cancelButtonText: 'না',
+    },
+  },
+}

+ 126 - 0
packages/locale/lang/ca.ts

@@ -0,0 +1,126 @@
+export default {
+  name: 'ca',
+  el: {
+    colorpicker: {
+      confirm: 'Confirmar',
+      clear: 'Netejar',
+    },
+    datepicker: {
+      now: 'Ara',
+      today: 'Avui',
+      cancel: 'Cancel·lar',
+      clear: 'Netejar',
+      confirm: 'Confirmar',
+      selectDate: 'Seleccionar data',
+      selectTime: 'Seleccionar hora',
+      startDate: 'Data Inici',
+      startTime: 'Hora Inici',
+      endDate: 'Data Final',
+      endTime: 'Hora Final',
+      prevYear: 'Any anterior',
+      nextYear: 'Pròxim Any',
+      prevMonth: 'Mes anterior',
+      nextMonth: 'Pròxim Mes',
+      year: '',
+      month1: 'Gener',
+      month2: 'Febrer',
+      month3: 'Març',
+      month4: 'Abril',
+      month5: 'Maig',
+      month6: 'Juny',
+      month7: 'Juliol',
+      month8: 'Agost',
+      month9: 'Setembre',
+      month10: 'Octubre',
+      month11: 'Novembre',
+      month12: 'Desembre',
+      // week: 'setmana',
+      weeks: {
+        sun: 'Dg',
+        mon: 'Dl',
+        tue: 'Dt',
+        wed: 'Dc',
+        thu: 'Dj',
+        fri: 'Dv',
+        sat: 'Ds',
+      },
+      months: {
+        jan: 'Gen',
+        feb: 'Febr',
+        mar: 'Març',
+        apr: 'Abr',
+        may: 'Maig',
+        jun: 'Juny',
+        jul: 'Jul',
+        aug: 'Ag',
+        sep: 'Set',
+        oct: 'Oct',
+        nov: 'Nov',
+        dec: 'Des',
+      },
+    },
+    select: {
+      loading: 'Carregant',
+      noMatch: 'No hi ha dades que coincideixin',
+      noData: 'Sense Dades',
+      placeholder: 'Seleccionar',
+    },
+    cascader: {
+      noMatch: 'No hi ha dades que coincideixin',
+      loading: 'Carregant',
+      placeholder: 'Seleccionar',
+      noData: 'Sense Dades',
+    },
+    pagination: {
+      goto: 'Anar a',
+      pagesize: '/pàgina',
+      total: 'Total {total}',
+      pageClassifier: '',
+      page: 'Page', // to be translated
+      prev: 'Go to previous page', // to be translated
+      next: 'Go to next page', // to be translated
+      currentPage: 'page {pager}', // to be translated
+      prevPages: 'Previous {pager} pages', // to be translated
+      nextPages: 'Next {pager} pages', // to be translated
+    },
+    messagebox: {
+      confirm: 'Acceptar',
+      cancel: 'Cancel·lar',
+      error: 'Entrada invàlida',
+    },
+    upload: {
+      deleteTip: 'premi eliminar per descartar',
+      delete: 'Eliminar',
+      preview: 'Vista Prèvia',
+      continue: 'Continuar',
+    },
+    table: {
+      emptyText: 'Sense Dades',
+      confirmFilter: 'Confirmar',
+      resetFilter: 'Netejar',
+      clearFilter: 'Tot',
+      sumText: 'Tot',
+    },
+    tree: {
+      emptyText: 'Sense Dades',
+    },
+    transfer: {
+      noMatch: 'No hi ha dades que coincideixin',
+      noData: 'Sense Dades',
+      titles: ['Llista 1', 'Llista 2'],
+      filterPlaceholder: 'Introdueix la paraula clau',
+      noCheckedFormat: '{total} ítems',
+      hasCheckedFormat: '{checked}/{total} seleccionats',
+    },
+    image: {
+      error: 'HA FALLAT',
+    },
+    pageHeader: {
+      title: 'Tornar',
+    },
+    popconfirm: {
+      confirmButtonText: 'Sí',
+      cancelButtonText: 'No',
+    },
+  },
+}

+ 165 - 0
packages/locale/lang/ckb.ts

@@ -0,0 +1,165 @@
+export default {
+  name: 'ckb',
+  el: {
+    colorpicker: {
+      confirm: 'باشە',
+      clear: 'پاککردنەوە',
+      defaultLabel: 'هەڵبژاردنی ڕەنگ',
+      description:
+        'ڕەنگی ئێستا {color}. ئینتەر دابگرە بۆ هەڵبژاردنی ڕەنگی نوێ.',
+    },
+    datepicker: {
+      now: 'ئێستا',
+      today: 'ئەمڕۆ',
+      cancel: 'پەشیمانبوونەوە',
+      clear: 'پاککردنەوە',
+      confirm: 'باشە',
+      dateTablePrompt:
+        'کلیلی ئاراستەکان بەکاربهێنەر بۆ هەڵبژاردنی ڕۆژی مانگەکە',
+      monthTablePrompt: 'کلیلی ئاراستەکان بەکاربهێنەر بۆ هەڵبژاردنی مانگ',
+      yearTablePrompt: 'کلیلی ئاراستەکان بەکاربهێنەر بۆ هەڵبژاردنی ساڵ',
+      selectedDate: 'بەرواری هەڵبژێردراو',
+      selectDate: 'هەڵبژاردنی بەروار',
+      selectTime: 'هەڵبژاردنی کات',
+      startDate: 'بەرواری دەستپێک',
+      startTime: 'کاتی دەستپێک',
+      endDate: 'بەرواری کۆتایی',
+      endTime: 'کاتی کۆتایی',
+      prevYear: 'ساڵی پێشوو',
+      nextYear: 'ساڵ داهاتوو',
+      prevMonth: 'مانگی پێشوو',
+      nextMonth: 'مانگی داهاتوو',
+      year: '',
+      month1: 'ڕێبەندان',
+      month2: 'ڕەشەمە',
+      month3: 'نەورۆز',
+      month4: 'گوڵان',
+      month5: 'جۆزەردان',
+      month6: 'پووشپەڕ',
+      month7: 'گەلاوێژ',
+      month8: 'خەرمانان',
+      month9: 'ڕەزبەر',
+      month10: 'گەڵاڕێزان',
+      month11: 'سەرماوەز',
+      month12: 'بەفرانبار',
+      week: 'هەفت',
+      weeks: {
+        sun: 'یەکشەممە',
+        mon: 'دووشەممە',
+        tue: 'سێشەممە',
+        wed: 'چوارشەممە',
+        thu: 'پێنجشەممە',
+        fri: 'هەینی',
+        sat: 'شەممە',
+      },
+      weeksFull: {
+        sun: 'یەکشەممە',
+        mon: 'دووشەممە',
+        tue: 'سێشەممە',
+        wed: 'چوارشەممە',
+        thu: 'پێنجشەممە',
+        fri: 'هەینی',
+        sat: 'شەممە',
+      },
+      months: {
+        jan: 'ڕێبەندان',
+        feb: 'ڕەشەمە',
+        mar: 'نەورۆز',
+        apr: 'گوڵان',
+        may: 'جۆزەردان',
+        jun: 'پووشپەڕ',
+        jul: 'گەلاوێژ',
+        aug: 'خەرمانان',
+        sep: 'ڕەزبەر',
+        oct: 'گەڵاڕێزان',
+        nov: 'سەرماوەز',
+        dec: 'بەفرانبار',
+      },
+    },
+    inputNumber: {
+      decrease: 'کەمکردنەوەی ژمارە',
+      increase: 'زیادکردنی ژمارە',
+    },
+    select: {
+      loading: 'بارکردن',
+      noMatch: 'هیچ داتایەکی هاوتا نیە',
+      noData: 'هیچ داتایەک نیە',
+      placeholder: 'هەڵبژاردن',
+    },
+    dropdown: {
+      toggleDropdown: 'کردنەوەو داخستنی کشاو',
+    },
+    cascader: {
+      noMatch: 'هیچ داتایەکی هاوتا نیە',
+      loading: 'بارکردن',
+      placeholder: 'هەڵبژاردن',
+      noData: 'هیچ داتایەک نیە',
+    },
+    pagination: {
+      goto: 'بڕۆ بۆ',
+      pagesize: '/لاپەڕە',
+      total: 'کۆی گشتیی {total}',
+      pageClassifier: '',
+      page: 'Page', // to be translated
+      prev: 'Go to previous page', // to be translated
+      next: 'Go to next page', // to be translated
+      currentPage: 'page {pager}', // to be translated
+      prevPages: 'Previous {pager} pages', // to be translated
+      nextPages: 'Next {pager} pages', // to be translated
+      deprecationWarning:
+        'بەکارهێنانی بەکارنەهێنراو دۆزراوەتەوە، تکایە بۆ وردەکاری زیاتر سەردانی بەڵگەنامەکانی el-pagination بکە',
+    },
+    dialog: {
+      close: 'داخستنی ئەم دیالۆگە',
+    },
+    drawer: {
+      close: 'داخستنی ئەم دیالۆگە',
+    },
+    messagebox: {
+      title: 'پەیام',
+      confirm: 'باشە',
+      cancel: 'پەشایمانبوونەوە',
+      error: 'داخلکردنی نایاسایی',
+      close: 'داخستنی ئەم دیالۆگە',
+    },
+    upload: {
+      deleteTip: 'فشار لەسەر سڕینەوە بکە بۆ لابردن',
+      delete: 'سڕینەوە',
+      preview: 'بینینەوە',
+      continue: 'بەردەوامبوون',
+    },
+    slider: {
+      defaultLabel: 'سلاید لە نێوان {min} و {max}',
+      defaultRangeStartLabel: 'بەهای دەستپێک هەلبژێرە',
+      defaultRangeEndLabel: 'بەهای کۆتایی هەلبژێرە',
+    },
+    table: {
+      emptyText: 'هیچ داتا نیە',
+      confirmFilter: 'دووپاتکردنەوە',
+      resetFilter: 'جێگیرکردنەوە',
+      clearFilter: 'هەموو',
+      sumText: 'کۆ',
+    },
+    tree: {
+      emptyText: 'هیچ داتا نیە',
+    },
+    transfer: {
+      noMatch: 'هیچ داتای هاوتا نیە',
+      noData: 'هیچ داتا نیە',
+      titles: ['لیستی 1', 'لیستی 2'], // to be translated
+      filterPlaceholder: 'کلیلەوشە داخڵ بکە', // to be translated
+      noCheckedFormat: '{total} دانە', // to be translated
+      hasCheckedFormat: '{checked}/{total} هەڵبژێردراوە', // to be translated
+    },
+    image: {
+      error: 'شکستی هێنا',
+    },
+    pageHeader: {
+      title: 'گەڕانەوە', // to be translated
+    },
+    popconfirm: {
+      confirmButtonText: 'بەڵێ',
+      cancelButtonText: 'نەخێر',
+    },
+  },
+}

+ 129 - 0
packages/locale/lang/cs.ts

@@ -0,0 +1,129 @@
+export default {
+  name: 'cs',
+  el: {
+    colorpicker: {
+      confirm: 'OK',
+      clear: 'Vymazat',
+    },
+    datepicker: {
+      now: 'Teď',
+      today: 'Dnes',
+      cancel: 'Zrušit',
+      clear: 'Vymazat',
+      confirm: 'OK',
+      selectDate: 'Vybrat datum',
+      selectTime: 'Vybrat čas',
+      startDate: 'Datum začátku',
+      startTime: 'Čas začátku',
+      endDate: 'Datum konce',
+      endTime: 'Čas konce',
+      prevYear: 'Předchozí rok',
+      nextYear: 'Příští rok',
+      prevMonth: 'Předchozí měsíc',
+      nextMonth: 'Příští měsíc',
+      day: 'Den',
+      week: 'Týden',
+      month: 'Měsíc',
+      year: 'Rok',
+      month1: 'Leden',
+      month2: 'Únor',
+      month3: 'Březen',
+      month4: 'Duben',
+      month5: 'Květen',
+      month6: 'Červen',
+      month7: 'Červenec',
+      month8: 'Srpen',
+      month9: 'Září',
+      month10: 'Říjen',
+      month11: 'Listopad',
+      month12: 'Prosinec',
+      weeks: {
+        sun: 'Ne',
+        mon: 'Po',
+        tue: 'Út',
+        wed: 'St',
+        thu: 'Čt',
+        fri: 'Pá',
+        sat: 'So',
+      },
+      months: {
+        jan: 'Led',
+        feb: 'Úno',
+        mar: 'Bře',
+        apr: 'Dub',
+        may: 'Kvě',
+        jun: 'Čer',
+        jul: 'Čvc',
+        aug: 'Srp',
+        sep: 'Zář',
+        oct: 'Říj',
+        nov: 'Lis',
+        dec: 'Pro',
+      },
+    },
+    select: {
+      loading: 'Načítání',
+      noMatch: 'Žádná shoda',
+      noData: 'Žádná data',
+      placeholder: 'Vybrat',
+    },
+    cascader: {
+      noMatch: 'Žádná shoda',
+      loading: 'Načítání',
+      placeholder: 'Vybrat',
+      noData: 'Žádná data',
+    },
+    pagination: {
+      goto: 'Jít na',
+      pagesize: 'na stranu',
+      total: 'Celkem {total}',
+      pageClassifier: '',
+      page: 'Page', // to be translated
+      prev: 'Go to previous page', // to be translated
+      next: 'Go to next page', // to be translated
+      currentPage: 'page {pager}', // to be translated
+      prevPages: 'Previous {pager} pages', // to be translated
+      nextPages: 'Next {pager} pages', // to be translated
+    },
+    messagebox: {
+      title: 'Zpráva',
+      confirm: 'OK',
+      cancel: 'Zrušit',
+      error: 'Neplatný vstup',
+    },
+    upload: {
+      deleteTip: 'Stisknout pro smazání',
+      delete: 'Vymazat',
+      preview: 'Náhled',
+      continue: 'Pokračovat',
+    },
+    table: {
+      emptyText: 'Žádná data',
+      confirmFilter: 'Potvrdit',
+      resetFilter: 'Resetovat',
+      clearFilter: 'Vše',
+      sumText: 'Celkem',
+    },
+    tree: {
+      emptyText: 'Žádná data',
+    },
+    transfer: {
+      noMatch: 'Žádná shoda',
+      noData: 'Žádná data',
+      titles: ['Seznam 1', 'Seznam 2'],
+      filterPlaceholder: 'Klíčové slovo',
+      noCheckedFormat: '{total} položek',
+      hasCheckedFormat: '{checked}/{total} vybráno',
+    },
+    image: {
+      error: 'FAILED', // to be translated
+    },
+    pageHeader: {
+      title: 'Back', // to be translated
+    },
+    popconfirm: {
+      confirmButtonText: 'Yes', // to be translated
+      cancelButtonText: 'No', // to be translated
+    },
+  },
+}

+ 0 - 0
packages/locale/lang/da.ts


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików