Pārlūkot izejas kodu

refactor(架构调整): 更换button与新增dailog

gemercheung 2 gadi atpakaļ
vecāks
revīzija
c0e5fa5963
100 mainītis faili ar 6046 papildinājumiem un 1115 dzēšanām
  1. 7 3
      docs/.vitepress/i18n/pages/component.json
  2. 29 0
      docs/.vitepress/plugins/tooltip.ts
  3. 7 0
      docs/.vitepress/vitepress/components/globals/vp-api-bool.vue
  4. 20 0
      docs/.vitepress/vitepress/components/globals/vp-api-enum.vue
  5. 33 0
      docs/.vitepress/vitepress/components/globals/vp-api-external.vue
  6. 40 0
      docs/.vitepress/vitepress/components/globals/vp-api-function.vue
  7. 7 0
      docs/.vitepress/vitepress/components/globals/vp-api-number.vue
  8. 14 0
      docs/.vitepress/vitepress/components/globals/vp-api-primitive.vue
  9. 21 0
      docs/.vitepress/vitepress/components/globals/vp-api-ref.vue
  10. 7 0
      docs/.vitepress/vitepress/components/globals/vp-api-string.vue
  11. 35 0
      docs/.vitepress/vitepress/components/globals/vp-api-typing.vue
  12. 20 3
      docs/.vitepress/vitepress/index.ts
  13. 3 3
      docs/.vitepress/vitepress/styles/css-vars.scss
  14. 9 0
      docs/components.d.ts
  15. 43 6
      docs/examples/button/basic.vue
  16. 16 12
      docs/examples/button/disabled.vue
  17. 0 62
      docs/examples/dailog/basic.vue
  18. 32 0
      docs/examples/dialog/align-center.vue
  19. 44 0
      docs/examples/dialog/basic-usage.vue
  20. 30 0
      docs/examples/dialog/centered-content.vue
  21. 96 0
      docs/examples/dialog/customization-content.vue
  22. 33 0
      docs/examples/dialog/customization-header.vue
  23. 40 0
      docs/examples/dialog/destroy-on-close.vue
  24. 23 0
      docs/examples/dialog/draggable-dialog.vue
  25. 47 0
      docs/examples/dialog/focus-trapping.vue
  26. 36 0
      docs/examples/dialog/nested-dialog.vue
  27. 4 4
      docs/examples/icon/basic.vue
  28. 147 5
      docs/zh-CN/component/button.md
  29. 166 0
      docs/zh-CN/component/dialog.md
  30. 0 6
      packages/components/advance/daikan/index.ts
  31. 0 18
      packages/components/advance/daikan/src/daikan.ts
  32. 0 7
      packages/components/advance/daikan/src/daikan.vue
  33. 6 6
      packages/components/advance/tag/src/metas/metasImage.vue
  34. 8 4
      packages/components/basic/button/index.ts
  35. 89 0
      packages/components/basic/button/src/button-custom.ts
  36. 15 0
      packages/components/basic/button/src/button-group.ts
  37. 24 0
      packages/components/basic/button/src/button-group.vue
  38. 113 10
      packages/components/basic/button/src/button.ts
  39. 61 20
      packages/components/basic/button/src/button.vue
  40. 5 0
      packages/components/basic/button/src/instance.ts
  41. 58 0
      packages/components/basic/button/src/use-button.ts
  42. 1 1
      packages/components/basic/button/style/css.ts
  43. 7 7
      packages/components/basic/config-provider/src/config-provider.ts
  44. 0 345
      packages/components/basic/dialog/__tests__/dialog.test.tsx
  45. 3 3
      packages/components/basic/dialog/index.ts
  46. 4 4
      packages/components/basic/dialog/src/dialog-content.ts
  47. 10 11
      packages/components/basic/dialog/src/dialog-content.vue
  48. 6 7
      packages/components/basic/dialog/src/dialog.vue
  49. 3 3
      packages/components/basic/dialog/style/css.ts
  50. 3 3
      packages/components/basic/dialog/style/index.ts
  51. 354 0
      packages/components/basic/focus-trap/__tests__/focus-trap.test.ts
  52. 56 0
      packages/components/basic/focus-trap/__tests__/utils.test.ts
  53. 7 0
      packages/components/basic/focus-trap/index.ts
  54. 328 0
      packages/components/basic/focus-trap/src/focus-trap.vue
  55. 24 0
      packages/components/basic/focus-trap/src/tokens.ts
  56. 196 0
      packages/components/basic/focus-trap/src/utils.ts
  57. 2 2
      packages/components/basic/icon/index.ts
  58. 4 2
      packages/components/basic/icon/src/icon.vue
  59. 4 0
      packages/components/basic/index.ts
  60. 2 2
      packages/components/basic/overlay/style/css.ts
  61. 2 2
      packages/components/basic/overlay/style/index.ts
  62. 5 0
      packages/hooks/index.ts
  63. 46 0
      packages/hooks/use-common-props/index.ts
  64. 87 0
      packages/hooks/use-draggable/index.ts
  65. 32 0
      packages/hooks/use-escape-keydown/index.ts
  66. 73 0
      packages/hooks/use-lockscreen/index.ts
  67. 58 0
      packages/hooks/use-teleport/index.ts
  68. 4 3
      packages/kankan-components/component.ts
  69. 3 3
      packages/kankan-components/index.ts
  70. 1 1
      packages/theme-chalk/gulpfile.ts
  71. 1 1
      packages/theme-chalk/src/base.scss
  72. 284 59
      packages/theme-chalk/src/button.scss
  73. 17 0
      packages/theme-chalk/src/color/index.scss
  74. 60 0
      packages/theme-chalk/src/common/_var.scss.bk
  75. 47 0
      packages/theme-chalk/src/common/popup.scss
  76. 122 0
      packages/theme-chalk/src/common/transition.scss
  77. 1419 0
      packages/theme-chalk/src/common/var.scss
  78. 2 1
      packages/theme-chalk/src/index.scss
  79. 167 0
      packages/theme-chalk/src/mixins/_button.scss
  80. 38 0
      packages/theme-chalk/src/mixins/_col.scss
  81. 66 0
      packages/theme-chalk/src/mixins/_var.scss
  82. 5 0
      packages/theme-chalk/src/mixins/config.scss
  83. 86 0
      packages/theme-chalk/src/mixins/function.scss
  84. 235 0
      packages/theme-chalk/src/mixins/mixins.scss
  85. 39 0
      packages/theme-chalk/src/mixins/utils.scss
  86. 92 0
      packages/theme-chalk/src/reset.scss
  87. 149 56
      packages/theme-chalk/src/var.scss
  88. 12 0
      packages/tokens/button.ts
  89. 14 0
      packages/tokens/dialog.ts
  90. 2 0
      packages/tokens/index.ts
  91. 1 0
      packages/utils/vue/icon.ts
  92. 1 0
      packages/utils/vue/index.ts
  93. 347 322
      playground/src/components/advance/tag/src/metas/metasImage.vue
  94. 2 2
      playground/src/components/basic/button/index.ts
  95. 15 15
      playground/src/components/basic/button/src/button.vue
  96. 59 48
      playground/src/components/basic/guide/index.vue
  97. 2 2
      playground/src/components/basic/icon/index.ts
  98. 28 23
      playground/src/components/basic/icon/src/icon.vue
  99. 21 18
      playground/src/components/basic/input/src/checkbox/checkbox.vue
  100. 0 0
      playground/src/components/basic/input/src/file/file.vue

+ 7 - 3
docs/.vitepress/i18n/pages/component.json

@@ -5,15 +5,19 @@
       "children": [
         {
           "link": "/icon",
-          "text": "图标"
+          "text": "Icon图标"
         },
         {
           "link": "/button",
-          "text": "按钮"
+          "text": "Button按钮"
         },
         {
           "link": "/audio",
-          "text": "音频"
+          "text": "Audio音频"
+        },
+        {
+          "link": "/dialog",
+          "text": "Dialog对话框"
         }
       ]
     },

+ 29 - 0
docs/.vitepress/plugins/tooltip.ts

@@ -0,0 +1,29 @@
+import type MarkdownIt from 'markdown-it'
+
+export default (md: MarkdownIt): void => {
+  md.renderer.rules.tooltip = (tokens, idx) => {
+    const token = tokens[idx]
+
+    return `<api-typing type="${token.content}" details="${token.info}" />`
+  }
+
+  md.inline.ruler.before('emphasis', 'tooltip', (state, silent) => {
+    const tooltipRegExp = /^\^\[([^\]]*)\](`[^`]*`)?/
+    const str = state.src.slice(state.pos, state.posMax)
+
+    if (!tooltipRegExp.test(str)) return false
+    if (silent) return true
+
+    const result = str.match(tooltipRegExp)
+
+    if (!result) return false
+
+    const token = state.push('tooltip', 'tooltip', 0)
+    token.content = result[1].replace(/\\\|/g, '|')
+    token.info = (result[2] || '').replace(/^`(.*)`$/, '$1')
+    token.level = state.level
+    state.pos += result[0].length
+
+    return true
+  })
+}

+ 7 - 0
docs/.vitepress/vitepress/components/globals/vp-api-bool.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts">
+import Primitive from './vp-api-primitive.vue'
+</script>
+
+<template>
+  <Primitive type="boolean" />
+</template>

+ 20 - 0
docs/.vitepress/vitepress/components/globals/vp-api-enum.vue

@@ -0,0 +1,20 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { isString } from '@vue/shared'
+import ApiTyping from './vp-api-typing.vue'
+
+const props = defineProps({
+  values: {
+    type: Array,
+    required: true,
+  },
+})
+
+const processString = (s: unknown) => (isString(s) ? `'${s}'` : s)
+
+const details = computed(() => props.values.map(processString).join(' | '))
+</script>
+
+<template>
+  <api-typing type="enum" :details="details" />
+</template>

+ 33 - 0
docs/.vitepress/vitepress/components/globals/vp-api-external.vue

@@ -0,0 +1,33 @@
+<script setup lang="ts">
+import VpLink from '../common/vp-link.vue'
+
+defineProps({
+  text: {
+    type: String,
+    required: true,
+  },
+  url: {
+    type: String,
+    required: true,
+  },
+  prefix: {
+    type: String,
+    default: '',
+  },
+})
+</script>
+
+<template>
+  <slot>
+    <span v-if="prefix" class="mr-1">{{ prefix }}</span>
+    <VpLink :href="url">
+      {{ text }}
+    </VpLink>
+  </slot>
+</template>
+
+<style scoped>
+:deep(.el-icon) {
+  font-size: 18px;
+}
+</style>

+ 40 - 0
docs/.vitepress/vitepress/components/globals/vp-api-function.vue

@@ -0,0 +1,40 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import ApiTyping from './vp-api-typing.vue'
+
+import type { PropType } from 'vue'
+
+type ParamType = [string, string]
+
+const props = defineProps({
+  /**
+   * @description params list, shape of Array<[key: string, value: string]>
+   */
+  params: {
+    type: Array as PropType<Array<ParamType>>,
+    default: () => [],
+  },
+  returns: {
+    type: String,
+    default: 'void',
+  },
+})
+
+const mappedParams = computed(() =>
+  props.params
+    .reduce((params, [key, val]) => {
+      let type = val
+      if (Array.isArray(val)) {
+        type = val.join(' | ')
+      }
+      return params.concat([`${key}: ${type}`])
+    }, [] as string[])
+    .join(', ')
+)
+
+const details = computed(() => `(${mappedParams.value}) => ${props.returns}`)
+</script>
+
+<template>
+  <api-typing type="Function" :details="details" />
+</template>

+ 7 - 0
docs/.vitepress/vitepress/components/globals/vp-api-number.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts">
+import Primitive from './vp-api-primitive.vue'
+</script>
+
+<template>
+  <Primitive type="number" />
+</template>

+ 14 - 0
docs/.vitepress/vitepress/components/globals/vp-api-primitive.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+defineProps({
+  type: {
+    type: String,
+    required: true,
+  },
+})
+</script>
+
+<template>
+  <code>
+    {{ type }}
+  </code>
+</template>

+ 21 - 0
docs/.vitepress/vitepress/components/globals/vp-api-ref.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import ApiTyping from './vp-api-typing.vue'
+
+const props = defineProps({
+  type: {
+    type: String,
+    required: true,
+  },
+  shallow: Boolean,
+})
+
+const type = computed(() => {
+  const wrapperType = props.shallow ? 'ShallowRef' : 'Ref'
+  return `${wrapperType}<${props.type}>`
+})
+</script>
+
+<template>
+  <ApiTyping type="Object" :details="type" />
+</template>

+ 7 - 0
docs/.vitepress/vitepress/components/globals/vp-api-string.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts">
+import Primitive from './vp-api-primitive.vue'
+</script>
+
+<template>
+  <Primitive type="string" />
+</template>

+ 35 - 0
docs/.vitepress/vitepress/components/globals/vp-api-typing.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import { Warning } from '@element-plus/icons-vue'
+
+defineProps({
+  type: String,
+  details: String,
+})
+</script>
+
+<template>
+  <span class="inline-flex items-center">
+    <code class="api-typing mr-1">
+      {{ type }}
+    </code>
+    <ClientOnly>
+      <ElTooltip v-if="details" effect="light" trigger="click">
+        <ElButton text :icon="Warning" class="p-2 text-4" />
+        <template #content>
+          <slot>
+            <div class="m-1">
+              <code
+                style="
+                  color: var(--code-tooltip-color);
+                  background-color: var(--code-tooltip-bg-color);
+                "
+              >
+                {{ details }}
+              </code>
+            </div>
+          </slot>
+        </template>
+      </ElTooltip>
+    </ClientOnly>
+  </span>
+</template>

+ 20 - 3
docs/.vitepress/vitepress/index.ts

@@ -1,10 +1,10 @@
 import 'normalize.css'
-// import 'element-plus/dist/index.css'
+import 'element-plus/dist/index.css'
 
 // for dev
 // reset
-import 'element-plus/theme-chalk/src/reset.scss'
-import 'element-plus/theme-chalk/src/index.scss'
+import '../../../packages/theme-chalk/src/reset.scss'
+import '../../../packages/theme-chalk/src/index.scss'
 // for dark mode
 import 'element-plus/theme-chalk/src/dark/css-vars.scss'
 
@@ -16,6 +16,15 @@ import 'uno.css'
 import VPApp from './components/vp-app.vue'
 import VPDemo from './components/vp-demo.vue'
 
+import ApiTyping from './components/globals/vp-api-typing.vue'
+import ApiFunctionType from './components/globals/vp-api-function.vue'
+import ApiBooleanType from './components/globals/vp-api-bool.vue'
+import ApiStringType from './components/globals/vp-api-string.vue'
+import ApiNumberType from './components/globals/vp-api-number.vue'
+import ApiRefType from './components/globals/vp-api-ref.vue'
+import ApiEnumType from './components/globals/vp-api-enum.vue'
+import ApiExternalType from './components/globals/vp-api-external.vue'
+
 import IconList from './components/globals/icons.vue'
 
 import type { Component } from 'vue'
@@ -26,4 +35,12 @@ export default VPApp
 export const globals: [string, Component][] = [
   ['Demo', VPDemo],
   ['IconList', IconList],
+  ['ApiTyping', ApiTyping],
+  ['FunctionType', ApiFunctionType],
+  ['EnumType', ApiEnumType],
+  ['BooleanType', ApiBooleanType],
+  ['StringType', ApiStringType],
+  ['NumberType', ApiNumberType],
+  ['RefType', ApiRefType],
+  ['ExternalType', ApiExternalType],
 ]

+ 3 - 3
docs/.vitepress/vitepress/styles/css-vars.scss

@@ -10,9 +10,9 @@
   --text-color-light: var(--el-text-color-regular);
   --text-color-lighter: var(--el-text-color-secondary);
 
-  --brand-color: var(--el-color-primary);
-  --brand-color-light: var(--el-color-primary-light-1);
-  --bg-brand-color: var(--el-color-primary-light-9);
+  --brand-color: var(--kk-color-primary);
+  --brand-color-light: var(--kk-color-primary-light-1);
+  --bg-brand-color: var(--kk-color-primary-light-9);
 
   --bg-color: var(--el-bg-color);
   --bg-color-rgb: 255, 255, 255;

+ 9 - 0
docs/components.d.ts

@@ -59,6 +59,15 @@ declare module '@vue/runtime-core' {
     ToggleSidebarBtn: typeof import('./.vitepress/vitepress/components/subnav/toggle-sidebar-btn.vue')['default']
     TopNavigationExample: typeof import('./.vitepress/vitepress/components/nav/top-navigation-example.vue')['default']
     VersionTag: typeof import('./.vitepress/vitepress/components/dev/VersionTag.vue')['default']
+    VpApiBool: typeof import('./.vitepress/vitepress/components/globals/vp-api-bool.vue')['default']
+    VpApiEnum: typeof import('./.vitepress/vitepress/components/globals/vp-api-enum.vue')['default']
+    VpApiExternal: typeof import('./.vitepress/vitepress/components/globals/vp-api-external.vue')['default']
+    VpApiFunction: typeof import('./.vitepress/vitepress/components/globals/vp-api-function.vue')['default']
+    VpApiNumber: typeof import('./.vitepress/vitepress/components/globals/vp-api-number.vue')['default']
+    VpApiPrimitive: typeof import('./.vitepress/vitepress/components/globals/vp-api-primitive.vue')['default']
+    VpApiRef: typeof import('./.vitepress/vitepress/components/globals/vp-api-ref.vue')['default']
+    VpApiString: typeof import('./.vitepress/vitepress/components/globals/vp-api-string.vue')['default']
+    VpApiTyping: typeof import('./.vitepress/vitepress/components/globals/vp-api-typing.vue')['default']
     VpApp: typeof import('./.vitepress/vitepress/components/vp-app.vue')['default']
     VpChangelog: typeof import('./.vitepress/vitepress/components/globals/vp-changelog.vue')['default']
     VpContent: typeof import('./.vitepress/vitepress/components/vp-content.vue')['default']

+ 43 - 6
docs/examples/button/basic.vue

@@ -1,12 +1,49 @@
 <template>
-  <div m4>
-    <UIButton :width="80" mr2>button1 </UIButton>
-    <UIButton type="primary" :width="80" mr2>button1 </UIButton>
-    <UIButton type="cancel" :width="100" mr2>button2</UIButton>
-    <UIButton type="submit" :width="100" mr2>button3</UIButton>
+  <div mb4>
+    <kk-button>Default</kk-button>
+    <kk-button type="primary">Primary</kk-button>
+    <kk-button type="success">Success</kk-button>
+    <kk-button type="info">Info</kk-button>
+    <kk-button type="warning">Warning</kk-button>
+    <kk-button type="danger">Danger</kk-button>
+  </div>
+
+  <div mb4>
+    <kk-button plain>Plain</kk-button>
+    <kk-button type="primary" plain>Primary</kk-button>
+    <kk-button type="success" plain>Success</kk-button>
+    <kk-button type="info" plain>Info</kk-button>
+    <kk-button type="warning" plain>Warning</kk-button>
+    <kk-button type="danger" plain>Danger</kk-button>
+  </div>
+
+  <div mb4>
+    <kk-button round>Round</kk-button>
+    <kk-button type="primary" round>Primary</kk-button>
+    <kk-button type="success" round>Success</kk-button>
+    <kk-button type="info" round>Info</kk-button>
+    <kk-button type="warning" round>Warning</kk-button>
+    <kk-button type="danger" round>Danger</kk-button>
+  </div>
+
+  <div>
+    <kk-button :icon="Search" circle />
+    <kk-button type="primary" :icon="Edit" circle />
+    <kk-button type="success" :icon="Check" circle />
+    <kk-button type="info" :icon="Message" circle />
+    <kk-button type="warning" :icon="Star" circle />
+    <kk-button type="danger" :icon="Delete" circle />
   </div>
 </template>
 
 <script lang="ts" setup>
-import { UIButton } from 'kankan-components'
+import { KkButton } from 'kankan-components'
+import {
+  Check,
+  Delete,
+  Edit,
+  Message,
+  Search,
+  Star,
+} from '@element-plus/icons-vue'
 </script>

+ 16 - 12
docs/examples/button/disabled.vue

@@ -1,19 +1,23 @@
 <template>
   <el-row class="mb-4">
-    <el-button disabled>Default</el-button>
-    <el-button type="primary" disabled>Primary</el-button>
-    <el-button type="success" disabled>Success</el-button>
-    <el-button type="info" disabled>Info</el-button>
-    <el-button type="warning" disabled>Warning</el-button>
-    <el-button type="danger" disabled>Danger</el-button>
+    <kk-button disabled>Default</kk-button>
+    <kk-button type="primary" disabled>Primary</kk-button>
+    <kk-button type="success" disabled>Success</kk-button>
+    <kk-button type="info" disabled>Info</kk-button>
+    <kk-button type="warning" disabled>Warning</kk-button>
+    <kk-button type="danger" disabled>Danger</kk-button>
   </el-row>
 
   <el-row>
-    <el-button plain disabled>Plain</el-button>
-    <el-button type="primary" plain disabled>Primary</el-button>
-    <el-button type="success" plain disabled>Success</el-button>
-    <el-button type="info" plain disabled>Info</el-button>
-    <el-button type="warning" plain disabled>Warning</el-button>
-    <el-button type="danger" plain disabled>Danger</el-button>
+    <kk-button plain disabled>Plain</kk-button>
+    <kk-button type="primary" plain disabled>Primary</kk-button>
+    <kk-button type="success" plain disabled>Success</kk-button>
+    <kk-button type="info" plain disabled>Info</kk-button>
+    <kk-button type="warning" plain disabled>Warning</kk-button>
+    <kk-button type="danger" plain disabled>Danger</kk-button>
   </el-row>
 </template>
+
+<script lang="ts" setup>
+import { KkButton } from 'kankan-components'
+</script>

+ 0 - 62
docs/examples/dailog/basic.vue

@@ -1,62 +0,0 @@
-<script setup lang="ts">
-import { onMounted, onUnmounted, provide, ref } from 'vue'
-import { UITag } from 'kankan-components'
-
-const tags = ref<any>([])
-
-onMounted(() => {
-  const __win = window as any
-  if (!__win.__sdk) {
-    const __sdk = (__win.__sdk = new __win.KanKan({
-      num: 'KJ-t-wOXfx2SDFy',
-      server: '#DEMOSEVER#',
-    }))
-    provide('__sdk', __sdk)
-
-    __sdk.TagManager.on('loaded', (data: any) =>
-      __sdk.TagManager.load((tags.value = data) && tags.value)
-    )
-    __sdk.mount('#scene').render()
-  }
-})
-onUnmounted(() => {
-  const __win = window as any
-  if (__win.__sdk) {
-    __win.__sdk = null
-  }
-})
-const handleTagview = ({ id }) => {
-  console.log('id', id)
-}
-</script>
-
-<template>
-  <div id="scene" class="scene">
-    <Teleport v-if="tags.length > 0" to=".kankan-plugins">
-      <div xui_tags_view>
-        <UITag
-          v-for="(item, index) in tags"
-          :key="index"
-          :tag="item"
-          @click="handleTagview"
-        />
-      </div>
-    </Teleport>
-  </div>
-</template>
-<style>
-html,
-body,
-#app {
-  width: 100%;
-  height: 100%;
-  padding: 0;
-  margin: 0;
-}
-.scene {
-  width: 100%;
-  height: 100%;
-  padding: 0;
-  margin: 0;
-}
-</style>

+ 32 - 0
docs/examples/dialog/align-center.vue

@@ -0,0 +1,32 @@
+<template>
+  <el-button text @click="centerDialogVisible = true">
+    Click to open the Dialog
+  </el-button>
+
+  <el-dialog
+    v-model="centerDialogVisible"
+    title="Warning"
+    width="30%"
+    align-center
+  >
+    <span>Open the dialog from the center from the screen</span>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="centerDialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="centerDialogVisible = false">
+          Confirm
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+<script lang="ts" setup>
+import { ref } from 'vue'
+
+const centerDialogVisible = ref(false)
+</script>
+<style scoped>
+.dialog-footer button:first-child {
+  margin-right: 10px;
+}
+</style>

+ 44 - 0
docs/examples/dialog/basic-usage.vue

@@ -0,0 +1,44 @@
+<template>
+  <kk-button text type="primary" @click="dialogVisible = true">
+    打开对话框
+  </kk-button>
+
+  <ui-dialog
+    v-model="dialogVisible"
+    title="Tips"
+    width="30%"
+    :before-close="handleClose"
+  >
+    <span>This is a message</span>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="dialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="dialogVisible = false">
+          Confirm
+        </el-button>
+      </span>
+    </template>
+  </ui-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { KkButton, UiDialog } from 'kankan-components'
+
+const dialogVisible = ref(false)
+
+const handleClose = (done: () => void) => {
+  // ElMessageBox.confirm('Are you sure to close this dialog?')
+  //   .then(() => {
+  //     done()
+  //   })
+  //   .catch(() => {
+  //     // catch error
+  //   })
+}
+</script>
+<style scoped>
+.dialog-footer button:first-child {
+  margin-right: 10px;
+}
+</style>

+ 30 - 0
docs/examples/dialog/centered-content.vue

@@ -0,0 +1,30 @@
+<template>
+  <el-button text @click="centerDialogVisible = true">
+    Click to open the Dialog
+  </el-button>
+
+  <el-dialog v-model="centerDialogVisible" title="Warning" width="30%" center>
+    <span>
+      It should be noted that the content will not be aligned in center by
+      default
+    </span>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="centerDialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="centerDialogVisible = false">
+          Confirm
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+<script lang="ts" setup>
+import { ref } from 'vue'
+
+const centerDialogVisible = ref(false)
+</script>
+<style scoped>
+.dialog-footer button:first-child {
+  margin-right: 10px;
+}
+</style>

+ 96 - 0
docs/examples/dialog/customization-content.vue

@@ -0,0 +1,96 @@
+<template>
+  <el-button text @click="dialogTableVisible = true">
+    open a Table nested Dialog
+  </el-button>
+
+  <el-dialog v-model="dialogTableVisible" title="Shipping address">
+    <el-table :data="gridData">
+      <el-table-column property="date" label="Date" width="150" />
+      <el-table-column property="name" label="Name" width="200" />
+      <el-table-column property="address" label="Address" />
+    </el-table>
+  </el-dialog>
+
+  <!-- Form -->
+  <el-button text @click="dialogFormVisible = true">
+    open a Form nested Dialog
+  </el-button>
+
+  <el-dialog v-model="dialogFormVisible" title="Shipping address">
+    <el-form :model="form">
+      <el-form-item label="Promotion name" :label-width="formLabelWidth">
+        <el-input v-model="form.name" autocomplete="off" />
+      </el-form-item>
+      <el-form-item label="Zones" :label-width="formLabelWidth">
+        <el-select v-model="form.region" placeholder="Please select a zone">
+          <el-option label="Zone No.1" value="shanghai" />
+          <el-option label="Zone No.2" value="beijing" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="dialogFormVisible = false">
+          Confirm
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { reactive, ref } from 'vue'
+
+const dialogTableVisible = ref(false)
+const dialogFormVisible = ref(false)
+const formLabelWidth = '140px'
+
+const form = reactive({
+  name: '',
+  region: '',
+  date1: '',
+  date2: '',
+  delivery: false,
+  type: [],
+  resource: '',
+  desc: '',
+})
+
+const gridData = [
+  {
+    date: '2016-05-02',
+    name: 'John Smith',
+    address: 'No.1518,  Jinshajiang Road, Putuo District',
+  },
+  {
+    date: '2016-05-04',
+    name: 'John Smith',
+    address: 'No.1518,  Jinshajiang Road, Putuo District',
+  },
+  {
+    date: '2016-05-01',
+    name: 'John Smith',
+    address: 'No.1518,  Jinshajiang Road, Putuo District',
+  },
+  {
+    date: '2016-05-03',
+    name: 'John Smith',
+    address: 'No.1518,  Jinshajiang Road, Putuo District',
+  },
+]
+</script>
+<style scoped>
+.el-button--text {
+  margin-right: 15px;
+}
+.el-select {
+  width: 300px;
+}
+.el-input {
+  width: 300px;
+}
+.dialog-footer button:first-child {
+  margin-right: 10px;
+}
+</style>

+ 33 - 0
docs/examples/dialog/customization-header.vue

@@ -0,0 +1,33 @@
+<template>
+  <el-button @click="visible = true">
+    Open Dialog with customized header
+  </el-button>
+  <el-dialog v-model="visible" :show-close="false">
+    <template #header="{ close, titleId, titleClass }">
+      <div class="my-header">
+        <h4 :id="titleId" :class="titleClass">This is a custom header!</h4>
+        <el-button type="danger" @click="close">
+          <el-icon class="el-icon--left"><CircleCloseFilled /></el-icon>
+          Close
+        </el-button>
+      </div>
+    </template>
+    This is dialog content.
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { ElButton, ElDialog } from 'element-plus'
+import { CircleCloseFilled } from '@element-plus/icons-vue'
+
+const visible = ref(false)
+</script>
+
+<style scoped>
+.my-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+}
+</style>

+ 40 - 0
docs/examples/dialog/destroy-on-close.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-button text @click="centerDialogVisible = true">
+    Click to open Dialog
+  </el-button>
+
+  <el-dialog
+    v-model="centerDialogVisible"
+    title="Notice"
+    width="30%"
+    destroy-on-close
+    center
+  >
+    <span>
+      Notice: before dialog gets opened for the first time this node and the one
+      bellow will not be rendered
+    </span>
+    <div>
+      <strong>Extra content (Not rendered)</strong>
+    </div>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="centerDialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="centerDialogVisible = false">
+          Confirm
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+
+const centerDialogVisible = ref(false)
+</script>
+<style scoped>
+.dialog-footer button:first-child {
+  margin-right: 10px;
+}
+</style>

+ 23 - 0
docs/examples/dialog/draggable-dialog.vue

@@ -0,0 +1,23 @@
+<template>
+  <el-button text @click="dialogVisible = true">
+    Click to open Dialog
+  </el-button>
+
+  <el-dialog v-model="dialogVisible" title="Tips" width="30%" draggable>
+    <span>It's a draggable Dialog</span>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="dialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="dialogVisible = false">
+          Confirm
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+
+const dialogVisible = ref(false)
+</script>

+ 47 - 0
docs/examples/dialog/focus-trapping.vue

@@ -0,0 +1,47 @@
+<template>
+  <el-button text @click="dialogVisible = true">
+    click to open the Dialog
+  </el-button>
+
+  <div>
+    <p>Close dialog and the input will be focused</p>
+    <el-input ref="inputRef" placeholder="Please input" />
+  </div>
+
+  <el-dialog
+    v-model="dialogVisible"
+    destroy-on-close
+    title="Tips"
+    width="30%"
+    @close-auto-focus="handleCloseAutoFocus"
+  >
+    <span>This is a message</span>
+    <el-divider />
+    <el-input placeholder="Initially focused" />
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="dialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="dialogVisible = false">
+          Confirm
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import type { ElInput } from 'element-plus'
+
+const dialogVisible = ref(false)
+const inputRef = ref<InstanceType<typeof ElInput>>()
+
+const handleCloseAutoFocus = () => {
+  inputRef.value?.focus()
+}
+</script>
+<style scoped>
+.dialog-footer button:first-child {
+  margin-right: 10px;
+}
+</style>

+ 36 - 0
docs/examples/dialog/nested-dialog.vue

@@ -0,0 +1,36 @@
+<template>
+  <el-button text @click="outerVisible = true">
+    open the outer Dialog
+  </el-button>
+
+  <el-dialog v-model="outerVisible" title="Outer Dialog">
+    <template #default>
+      <el-dialog
+        v-model="innerVisible"
+        width="30%"
+        title="Inner Dialog"
+        append-to-body
+      />
+    </template>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="outerVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="innerVisible = true">
+          open the inner Dialog
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+
+const outerVisible = ref(false)
+const innerVisible = ref(false)
+</script>
+<style scoped>
+.dialog-footer button:first-child {
+  margin-right: 10px;
+}
+</style>

+ 4 - 4
docs/examples/icon/basic.vue

@@ -1,11 +1,11 @@
 <template>
   <div m4>
-    <ui-icon type="del" mr2 />
-    <ui-icon type="del" :size="16" mr2 />
-    <ui-icon type="del" :size="20" mr2 />
+    <kk-icon type="del" mr2 />
+    <kk-icon type="del" :size="16" mr2 />
+    <kk-icon type="del" :size="20" mr2 />
   </div>
 </template>
 
 <script lang="ts" setup>
-import { UiIcon } from 'kankan-components'
+import { KkIcon } from 'kankan-components'
 </script>

+ 147 - 5
docs/zh-CN/component/button.md

@@ -1,16 +1,158 @@
 ---
-title: Button
+title: Button 按钮
 lang: zh-CN
 ---
 
-# 按钮
+## 基础用法
 
-一般按钮使用如下
+:::demo 使用 type、plain、round 和 circle 来定义按钮的样式
 
-## 基本用法
+button/basic
+
+:::
+
+## 禁用状态
+
+你可以使用 disabled 属性来定义按钮是否被禁用。
+
+:::demo 使用 disabled 属性来控制按钮是否为禁用状态。 该属性接受一个 Boolean 类型的值。
+
+button/disabled
+
+:::
+
+## 链接按钮
 
 :::demo
 
-button/basic
+button/link
+
+:::
+
+## 文字按钮
+
+没有边框和背景色的按钮。
+
+:::demo
+
+button/text
+
+:::
+
+## 图标按钮
+
+使用图标为按钮添加更多的含义。 你也可以单独使用图标不添加文字来节省显示区域占用。
+
+:::demo 使用 icon 属性来为按钮添加图标。 您可以在我们的 Icon 组件中找到所需图标。 通过向右方添加<i>标签来添加图标, 你也可以使用自定义图标。
+
+button/icon
+
+:::
+
+## 按钮组
+
+以按钮组的方式出现,常用于多项类似操作。
+
+:::demo 使用 <kk-button-group> 对多个按钮分组。
+
+button/group
+
+:::
+
+## 加载状态按钮
+
+点击按钮来加载数据,并向用户反馈加载状态。
+
+通过设置 loading 属性为 true 来显示加载中状态。
+
+:::tip
+
+您可以使用 `loading` 插槽或 `loadingIcon` 属性自定义您的 loading 图标
+ps: `loading` 插槽优先级高于`loadingIcon` 属性
+
+:::
+
+:::demo
+
+button/loading
+
+:::
+
+## Sizes
+
+Besides default size, Button component provides three additional sizes for you to choose among different scenarios.
+
+:::demo Use attribute `size` to set additional sizes with `large`, `small`.
+
+button/size
+
+:::
+
+## Custom Color<VersionTag version="beta" />
+
+You can custom button color.
+
+We will calculate hover color & active color automatically.
+
+:::demo
+
+button/custom
 
 :::
+
+## Button API
+
+### Button 属性
+
+| 属性名                             | 说明                                                                    | 类型                                                                            | 默认值  |
+| ---------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------- |
+| size                               | button size                                                             | ^[enum]`'large'\| 'default'\| 'small'`                                          | —       |
+| type                               | button type                                                             | ^[enum]`'primary'\| 'success'\| 'warning'\| 'danger'\| 'info'\| 'text'(delete)` | —       |
+| plain                              | determine whether it's a plain button                                   | ^[boolean]                                                                      | false   |
+| text<VersionTag version="2.2.0" /> | determine whether it's a text button                                    | ^[boolean]                                                                      | false   |
+| bg<VersionTag version="2.2.0" />   | determine whether the text button background color is always on         | ^[boolean]                                                                      | false   |
+| link<VersionTag version="2.2.1" /> | determine whether it's a link button                                    | ^[boolean]                                                                      | false   |
+| round                              | determine whether it's a round button                                   | ^[boolean]                                                                      | false   |
+| circle                             | determine whether it's a circle button                                  | ^[boolean]                                                                      | false   |
+| loading                            | determine whether it's loading                                          | ^[boolean]                                                                      | false   |
+| loading-icon                       | customize loading icon component                                        | ^[string] / ^[Component]                                                        | Loading |
+| disabled                           | disable the button                                                      | ^[boolean]                                                                      | false   |
+| icon                               | icon component                                                          | ^[string] / ^[Component]                                                        | —       |
+| autofocus                          | same as native button's `autofocus`                                     | ^[boolean]                                                                      | false   |
+| native-type                        | same as native button's `type`                                          | ^[enum]`'button'\| 'submit'\| 'reset'`                                          | button  |
+| auto-insert-space                  | automatically insert a space between two chinese characters             | ^[boolean]                                                                      | —       |
+| color                              | custom button color, automatically calculate `hover` and `active` color | ^[string]                                                                       | —       |
+| dark                               | dark mode, which automatically converts `color` to dark mode colors     | ^[boolean]                                                                      | false   |
+
+### Button Slots
+
+| Name    | Description                 |
+| ------- | --------------------------- |
+| default | customize default content   |
+| loading | customize loading component |
+| icon    | customize icon component    |
+
+### Button Exposes
+
+| Name           | Description          | Type                                                                                                           |
+| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------- |
+| ref            | button html element  | ^[object]`Ref<HTMLButtonElement>`                                                                              |
+| size           | button size          | ^[object]`ComputedRef<'' \| 'small' \| 'default' \| 'large'>`                                                  |
+| type           | button type          | ^[object]`ComputedRef<'' \| 'default' \| 'primary' \| 'success' \| 'warning' \| 'info' \| 'danger' \| 'text'>` |
+| disabled       | button disabled      | ^[object]`ComputedRef<boolean>`                                                                                |
+| shouldAddSpace | whether adding space | ^[object]`ComputedRef<boolean>`                                                                                |
+
+## ButtonGroup API
+
+### ButtonGroup Attributes
+
+| Name | Description                                      | Type                                                           | Default |
+| ---- | ------------------------------------------------ | -------------------------------------------------------------- | ------- |
+| size | control the size of buttons in this button-group | ^[enum]`'large'\| 'default'\| 'small'`                         | —       |
+| type | control the type of buttons in this button-group | ^[enum]`'primary'\| 'success'\| 'warning'\| 'danger'\| 'info'` | —       |
+
+### ButtonGroup Slots
+
+| Name    | Description                    | Subtags |
+| ------- | ------------------------------ | ------- |
+| default | customize button group content | Button  |

+ 166 - 0
docs/zh-CN/component/dialog.md

@@ -0,0 +1,166 @@
+---
+title: Dialog对话框
+lang: zh-CN
+---
+
+# Dialog 对话框
+
+## 基础用法
+
+:::demo 需要设置 model-value / v-model 属性,它接收 Boolean,当为 true 时显示 Dialog。 Dialog 分为两个部分:body 和 footer,footer 需要具名为 footer 的 slot。 title 属性用于定义标题,它是可选的,默认值为空。 最后,本例还展示了 before-close 的用法。
+
+dialog/basic-usage
+
+:::
+
+:::tip
+
+`before-close` only works when user clicks the close icon or the backdrop. If you have buttons that close the Dialog in the `footer` named slot, you can add what you would do with `before-close` in the buttons' click event handler.
+
+:::
+
+## Customized Content
+
+The content of Dialog can be anything, even a table or a form. This example shows how to use Element Plus Table and Form with Dialog.
+
+:::demo
+
+dialog/customization-content
+
+:::
+
+## Customized Header
+
+The `header` slot can be used to customize the area where the title is displayed. In order to maintain accessibility, use the `title` attribute in addition to using this slot, or use the `titleId` slot property to specify which element should be read out as the dialog title.
+
+:::demo
+
+dialog/customization-header
+
+:::
+
+## Nested Dialog
+
+If a Dialog is nested in another Dialog, `append-to-body` is required.
+
+:::demo Normally we do not recommend using nested Dialog. If you need multiple Dialogs rendered on the page, you can simply flat them so that they're siblings to each other. If you must nest a Dialog inside another Dialog, set `append-to-body` of the nested Dialog to true, and it will append to body instead of its parent node, so both Dialogs can be correctly rendered.
+
+dialog/nested-dialog
+
+:::
+
+## Centered content
+
+Dialog's content can be centered.
+
+:::demo Setting `center` to `true` will center dialog's header and footer horizontally. `center` only affects Dialog's header and footer. The body of Dialog can be anything, so sometimes it may not look good when centered. You need to write some CSS if you wish to center the body as well.
+
+dialog/centered-content
+
+:::
+
+:::tip
+
+The content of Dialog is lazily rendered, which means the default slot is not rendered onto the DOM until it is firstly opened. Therefore, if you need to perform a DOM manipulation or access a component using `ref`, do it in the `open` event callback.
+
+:::
+
+## Align Center dialog
+
+Open dialog from the center of the screen.
+
+:::demo Setting `align-center` to `true` will center the dialog both horizontally and vertically. The prop `top` will not work at the same time because the dialog is vertically centered in a flexbox.
+
+dialog/align-center
+
+:::
+
+## Destroy on Close
+
+When this is feature is enabled, the content under default slot will be destroyed with a `v-if` directive. Enable this when you have perf concerns.
+
+:::demo Note that by enabling this feature, the content will not be rendered before `transition.beforeEnter` dispatched, there will only be `overlay` `header(if any)` `footer(if any)`.
+
+dialog/destroy-on-close
+
+:::
+
+## Draggable Dialog
+
+Try to drag the `header` part.
+
+:::demo Set `draggable` to `true` to drag.
+
+dialog/draggable-dialog
+
+:::
+
+:::tip
+
+When using `modal` = false, please make sure that `append-to-body` was set to **true**, because `Dialog` was positioned by `position: relative`, when `modal` gets removed, `Dialog` will position itself based on the current position in the DOM, instead of `Document.Body`, thus the style will be messed up.
+
+:::
+
+## Attributes
+
+| Name                          | Description                                                                                       | Type                                              | Accepted Values | Default |
+| ----------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------- | --------------- | ------- |
+| model-value / v-model         | visibility of Dialog                                                                              | boolean                                           | —               | —       |
+| title                         | title of Dialog. Can also be passed with a named slot (see the following table)                   | string                                            | —               | —       |
+| width                         | width of Dialog                                                                                   | string / number                                   | —               | 50%     |
+| fullscreen                    | whether the Dialog takes up full screen                                                           | boolean                                           | —               | false   |
+| top                           | value for `margin-top` of Dialog CSS                                                              | string                                            | —               | 15vh    |
+| modal                         | whether a mask is displayed                                                                       | boolean                                           | —               | true    |
+| append-to-body                | whether to append Dialog itself to body. A nested Dialog should have this attribute set to `true` | boolean                                           | —               | false   |
+| lock-scroll                   | whether scroll of body is disabled while Dialog is displayed                                      | boolean                                           | —               | true    |
+| custom-class<DeprecatedTag /> | custom class names for Dialog                                                                     | string                                            | —               | —       |
+| open-delay                    | Time(milliseconds) before open                                                                    | number                                            | —               | 0       |
+| close-delay                   | Time(milliseconds) before close                                                                   | number                                            | —               | 0       |
+| close-on-click-modal          | whether the Dialog can be closed by clicking the mask                                             | boolean                                           | —               | true    |
+| close-on-press-escape         | whether the Dialog can be closed by pressing ESC                                                  | boolean                                           | —               | true    |
+| show-close                    | whether to show a close button                                                                    | boolean                                           | —               | true    |
+| before-close                  | callback before Dialog closes, and it will prevent Dialog from closing                            | Function(done) (done is used to close the Dialog) | —               | —       |
+| draggable                     | enable dragging feature for Dialog                                                                | boolean                                           | —               | false   |
+| center                        | whether to align the header and footer in center                                                  | boolean                                           | —               | false   |
+| align-center                  | whether to align the dialog both horizontally and vertically                                      | boolean                                           | —               | false   |
+| destroy-on-close              | Destroy elements in Dialog when closed                                                            | boolean                                           | —               | false   |
+
+:::warning
+
+`custom-class` has been **deprecated**, and **will be** removed in<VersionTag version="2.3.0" />, please use `class`.
+
+:::
+
+## Slots
+
+| Name                   | Description                                                                                           |
+| ---------------------- | ----------------------------------------------------------------------------------------------------- |
+| —                      | content of Dialog                                                                                     |
+| header                 | content of the Dialog header; Replacing this removes the title, but does not remove the close button. |
+| title<DeprecatedTag /> | Works the same as the header slot. Use that instead.                                                  |
+| footer                 | content of the Dialog footer                                                                          |
+
+## Events
+
+| Name             | Description                                      | Parameters |
+| ---------------- | ------------------------------------------------ | ---------- |
+| open             | triggers when the Dialog opens                   | —          |
+| opened           | triggers when the Dialog opening animation ends  | —          |
+| close            | triggers when the Dialog closes                  | —          |
+| closed           | triggers when the Dialog closing animation ends  | —          |
+| open-auto-focus  | triggers after Dialog opens and content focused  | —          |
+| close-auto-focus | triggers after Dialog closed and content focused | —          |
+
+## FAQ
+
+#### Using dialog in SFC, the scope style does not take effect.
+
+Typical issue: [#10515](https://github.com/element-plus/element-plus/issues/10515)
+
+PS: Since the dialog is rendered using `Teleport`, the style of the root node is recommended to be written globally.
+
+#### When the dialog is displayed and hidden, there is a situation where the page elements are displaced back and forth.
+
+Typical issue: [#10481](https://github.com/element-plus/element-plus/issues/10481)
+
+PS: It is recommended to place the scroll area inside a vue mounted node, e.g. `<div id="app" />`, and use the `overflow: hidden` style for the body.

+ 0 - 6
packages/components/advance/daikan/index.ts

@@ -1,6 +0,0 @@
-import { withInstall } from '@kankan-components/utils'
-import daikan from './src/daikan.vue'
-
-export const UIDaiKan = withInstall(daikan)
-
-export default UIDaiKan

+ 0 - 18
packages/components/advance/daikan/src/daikan.ts

@@ -1,18 +0,0 @@
-import { buildProps } from '@kankan-components/utils'
-import type { ExtractPropTypes, PropType } from 'vue'
-
-export const daikanProps = buildProps({
-  userInfo: {},
-  socketUrl: {
-    type: String,
-    required: true,
-  },
-} as const)
-export type DaikanProps = ExtractPropTypes<typeof daikanProps>
-
-export const tagEmits = {
-  click: () => true,
-  mouseEnter: () => true,
-  mouseLeave: () => true,
-}
-export type TagEmits = typeof tagEmits

+ 0 - 7
packages/components/advance/daikan/src/daikan.vue

@@ -1,7 +0,0 @@
-<template><div :class="[ns.b()]">test</div></template>
-
-<script lang="ts" setup>
-import { useNamespace } from '@kankan-components/hooks'
-
-const ns = useNamespace('daikan')
-</script>

+ 6 - 6
packages/components/advance/tag/src/metas/metasImage.vue

@@ -11,14 +11,14 @@
         class="ctrl-btn left-btn"
         @click.stop="changeImage('pre')"
       >
-        <ui-icon type="left" />
+        <kk-icon type="left" />
       </div>
       <div
         v-if="currentIndex < data.length - 1"
         class="ctrl-btn right-btn"
         @click.stop="changeImage('next')"
       >
-        <ui-icon type="right" />
+        <kk-icon type="right" />
       </div>
     </div>
     <div class="over-box">
@@ -35,9 +35,9 @@
           }%);background-image:url(${changeUrl(i.src)});`"
         />
       </div>
-      <ui-icon class="loading-icon" type="_loading_" />
+      <kk-icon class="loading-icon" type="_loading_" />
       <!-- <div class="del-btn">
-                <ui-icon type="del"></ui-icon>
+                <kk-icon type="del"></kk-icon>
             </div> -->
     </div>
     <div class="continue">
@@ -54,7 +54,7 @@
 <script lang="ts">
 import { defineComponent, inject, ref } from 'vue'
 import { buildProps } from '@kankan-components/utils'
-import { UIIcon } from '@kankan-components/components'
+import { KkIcon } from '@kankan-components/components'
 import type { PropType } from 'vue'
 
 const currentIndex = ref(0)
@@ -85,7 +85,7 @@ const props = buildProps({
 export default defineComponent({
   name: 'MetaImage',
   components: {
-    'ui-icon': UIIcon,
+    'kk-icon': KkIcon,
   },
   props,
   setup() {

+ 8 - 4
packages/components/basic/button/index.ts

@@ -1,8 +1,12 @@
-import { withInstall } from '@kankan-components/utils'
+import { withInstall, withNoopInstall } from '@kankan-components/utils'
 import Button from './src/button.vue'
+import ButtonGroup from './src/button-group.vue'
 
-export const UIButton = withInstall(Button)
-
-export default UIButton
+export const KkButton = withInstall(Button, {
+  ButtonGroup,
+})
+export const KkButtonGroup = withNoopInstall(ButtonGroup)
+export default KkButton
 
 export * from './src/button'
+export type { ButtonInstance, ButtonGroupInstance } from './src/instance'

+ 89 - 0
packages/components/basic/button/src/button-custom.ts

@@ -0,0 +1,89 @@
+import { computed } from 'vue'
+import { TinyColor } from '@ctrl/tinycolor'
+import { useDisabled, useNamespace } from '@kankan-components/hooks'
+import type { ButtonProps } from './button'
+
+export function darken(color: TinyColor, amount = 20) {
+  return color.mix('#141414', amount).toString()
+}
+
+export function useButtonCustomStyle(props: ButtonProps) {
+  const _disabled = useDisabled()
+  const ns = useNamespace('button')
+
+  // calculate hover & active color by custom color
+  // only work when custom color
+  return computed(() => {
+    let styles: Record<string, string> = {}
+
+    const buttonColor = props.color
+
+    if (buttonColor) {
+      const color = new TinyColor(buttonColor)
+      const activeBgColor = props.dark
+        ? color.tint(20).toString()
+        : darken(color, 20)
+
+      if (props.plain) {
+        styles = ns.cssVarBlock({
+          'bg-color': props.dark
+            ? darken(color, 90)
+            : color.tint(90).toString(),
+          'text-color': buttonColor,
+          'border-color': props.dark
+            ? darken(color, 50)
+            : color.tint(50).toString(),
+          'hover-text-color': `var(${ns.cssVarName('color-white')})`,
+          'hover-bg-color': buttonColor,
+          'hover-border-color': buttonColor,
+          'active-bg-color': activeBgColor,
+          'active-text-color': `var(${ns.cssVarName('color-white')})`,
+          'active-border-color': activeBgColor,
+        })
+
+        if (_disabled.value) {
+          styles[ns.cssVarBlockName('disabled-bg-color')] = props.dark
+            ? darken(color, 90)
+            : color.tint(90).toString()
+          styles[ns.cssVarBlockName('disabled-text-color')] = props.dark
+            ? darken(color, 50)
+            : color.tint(50).toString()
+          styles[ns.cssVarBlockName('disabled-border-color')] = props.dark
+            ? darken(color, 80)
+            : color.tint(80).toString()
+        }
+      } else {
+        const hoverBgColor = props.dark
+          ? darken(color, 30)
+          : color.tint(30).toString()
+        const textColor = color.isDark()
+          ? `var(${ns.cssVarName('color-white')})`
+          : `var(${ns.cssVarName('color-black')})`
+        styles = ns.cssVarBlock({
+          'bg-color': buttonColor,
+          'text-color': textColor,
+          'border-color': buttonColor,
+          'hover-bg-color': hoverBgColor,
+          'hover-text-color': textColor,
+          'hover-border-color': hoverBgColor,
+          'active-bg-color': activeBgColor,
+          'active-border-color': activeBgColor,
+        })
+
+        if (_disabled.value) {
+          const disabledButtonColor = props.dark
+            ? darken(color, 50)
+            : color.tint(50).toString()
+          styles[ns.cssVarBlockName('disabled-bg-color')] = disabledButtonColor
+          styles[ns.cssVarBlockName('disabled-text-color')] = props.dark
+            ? 'rgba(255, 255, 255, 0.5)'
+            : `var(${ns.cssVarName('color-white')})`
+          styles[ns.cssVarBlockName('disabled-border-color')] =
+            disabledButtonColor
+        }
+      }
+    }
+
+    return styles
+  })
+}

+ 15 - 0
packages/components/basic/button/src/button-group.ts

@@ -0,0 +1,15 @@
+import { buttonProps } from './button'
+
+import type { ExtractPropTypes } from 'vue'
+
+export const buttonGroupProps = {
+  /**
+   * @description control the size of buttons in this button-group
+   */
+  size: buttonProps.size,
+  /**
+   * @description control the type of buttons in this button-group
+   */
+  type: buttonProps.type,
+} as const
+export type ButtonGroupProps = ExtractPropTypes<typeof buttonGroupProps>

+ 24 - 0
packages/components/basic/button/src/button-group.vue

@@ -0,0 +1,24 @@
+<template>
+  <div :class="`${ns.b('group')}`">
+    <slot />
+  </div>
+</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'
+
+defineOptions({
+  name: 'ElButtonGroup',
+})
+const props = defineProps(buttonGroupProps)
+provide(
+  buttonGroupContextKey,
+  reactive({
+    size: toRef(props, 'size'),
+    type: toRef(props, 'type'),
+  })
+)
+const ns = useNamespace('button')
+</script>

+ 113 - 10
packages/components/basic/button/src/button.ts

@@ -1,23 +1,126 @@
+import { useSizeProp } from '@kankan-components/hooks'
 import { buildProps, definePropType } from '@kankan-components/utils'
-import type { ExtractPropTypes } from 'vue'
-import type button from './button.vue'
+import { Loading } from '@element-plus/icons-vue'
+import type { Component, ExtractPropTypes } from 'vue'
+
+const iconPropType = definePropType<string | Component>([
+  String,
+  Object,
+  Function,
+])
+
+export const buttonTypes = [
+  'default',
+  'primary',
+  'success',
+  'warning',
+  'info',
+  'danger',
+  /**
+   * @deprecated
+   * Text type will be deprecated in the next major version (3.0.0)
+   */
+  'text',
+  '',
+] as const
+export const buttonNativeTypes = ['button', 'submit', 'reset'] as const
 
 export const buttonProps = buildProps({
+  /**
+   * @description button size
+   */
+  size: useSizeProp,
+  /**
+   * @description disable the button
+   */
+  disabled: Boolean,
+  /**
+   * @description button type
+   */
   type: {
     type: String,
-    default: 'normal',
+    values: buttonTypes,
+    default: '',
+  },
+  /**
+   * @description icon component
+   */
+  icon: {
+    type: iconPropType,
   },
-  color: {
+  /**
+   * @description native button type
+   */
+  nativeType: {
     type: String,
+    values: buttonNativeTypes,
+    default: 'button',
   },
-  width: {
-    type: definePropType<number | string>([Number, String]),
+  /**
+   * @description determine whether it's loading
+   */
+  loading: Boolean,
+  /**
+   * @description customize loading icon component
+   */
+  loadingIcon: {
+    type: iconPropType,
+    default: () => Loading,
   },
-  icon: {
-    type: String,
+  /**
+   * @description determine whether it's a plain button
+   */
+  plain: Boolean,
+  /**
+   * @description determine whether it's a text button
+   */
+  text: Boolean,
+  /**
+   * @description determine whether it's a link button
+   */
+  link: Boolean,
+  /**
+   * @description determine whether the text button background color is always on
+   */
+  bg: Boolean,
+  /**
+   * @description native button autofocus
+   */
+  autofocus: Boolean,
+  /**
+   * @description determine whether it's a round button
+   */
+  round: Boolean,
+  /**
+   * @description determine whether it's a circle button
+   */
+  circle: Boolean,
+  /**
+   * @description custom button color, automatically calculate `hover` and `active` color
+   */
+  color: String,
+  /**
+   * @description dark mode, which automatically converts `color` to dark mode colors
+   */
+  dark: Boolean,
+  /**
+   * @description automatically insert a space between two chinese characters
+   */
+  autoInsertSpace: {
+    type: Boolean,
+    default: undefined,
   },
-})
+} as const)
+export const buttonEmits = {
+  click: (evt: MouseEvent) => evt instanceof MouseEvent,
+}
 
 export type ButtonProps = ExtractPropTypes<typeof buttonProps>
+export type ButtonEmits = typeof buttonEmits
+
+export type ButtonType = ButtonProps['type']
+export type ButtonNativeType = ButtonProps['nativeType']
 
-export type ButtonInstance = InstanceType<typeof button>
+export interface ButtonConfigContext {
+  autoInsertSpace?: boolean
+}

+ 61 - 20
packages/components/basic/button/src/button.vue

@@ -1,33 +1,74 @@
 <template>
-  <button class="ui-button" :class="className" :style="style">
-    <UIIcon v-if="icon" :type="icon" class="ui-button-icon" />
-    <slot />
+  <button
+    ref="_ref"
+    :class="[
+      ns.b(),
+      ns.m(_type),
+      ns.m(_size),
+      ns.is('disabled', _disabled),
+      ns.is('loading', loading),
+      ns.is('plain', plain),
+      ns.is('round', round),
+      ns.is('circle', circle),
+      ns.is('text', text),
+      ns.is('link', link),
+      ns.is('has-bg', bg),
+    ]"
+    :aria-disabled="_disabled || loading"
+    :disabled="_disabled || loading"
+    :autofocus="autofocus"
+    :type="nativeType"
+    :style="buttonStyle"
+    @click="handleClick"
+  >
+    <template v-if="loading">
+      <slot v-if="$slots.loading" name="loading" />
+      <el-icon v-else :class="ns.is('loading')">
+        <component :is="loadingIcon" />
+      </el-icon>
+    </template>
+    <kk-icon v-else-if="icon || $slots.icon">
+      <component :is="icon" v-if="icon" />
+      <slot v-else name="icon" />
+    </kk-icon>
+    <span
+      v-if="$slots.default"
+      :class="{ [ns.em('text', 'expand')]: shouldAddSpace }"
+    >
+      <slot />
+    </span>
   </button>
 </template>
 
 <script lang="ts" setup>
-import { computed, defineProps } from 'vue'
-import { normalizeUnitToStyle } from '@kankan-components/utils'
-import UIIcon from '@kankan-components/components/basic/icon'
-import { buttonProps } from './button'
+import { KkIcon } from '@kankan-components/components/basic/icon'
+import { useNamespace } from '@kankan-components/hooks'
+import { useButton } from './use-button'
+import { buttonEmits, buttonProps } from './button'
+import { useButtonCustomStyle } from './button-custom'
 
 defineOptions({
-  name: 'UiButton',
+  name: 'KkButton',
 })
-const props = defineProps(buttonProps)
 
-const custom = `customize`
-const className = computed(() => (props.color ? custom : props.type))
+const props = defineProps(buttonProps)
+const emit = defineEmits(buttonEmits)
 
-const style = computed(() => {
-  const style = {
-    width: props.width ? normalizeUnitToStyle(props.width) : 'auto',
-    '--color': '',
-  }
+const buttonStyle = useButtonCustomStyle(props)
+const ns = useNamespace('button')
+const { _ref, _size, _type, _disabled, shouldAddSpace, handleClick } =
+  useButton(props, emit)
 
-  if (className.value === custom) {
-    style['--color'] = props.color || ''
-  }
-  return style
+defineExpose({
+  /** @description button html element */
+  ref: _ref,
+  /** @description button size */
+  size: _size,
+  /** @description button type */
+  type: _type,
+  /** @description button disabled */
+  disabled: _disabled,
+  /** @description whether adding space */
+  shouldAddSpace,
 })
 </script>

+ 5 - 0
packages/components/basic/button/src/instance.ts

@@ -0,0 +1,5 @@
+import type Button from './button.vue'
+import type ButtonGroup from './button-group.vue'
+
+export type ButtonInstance = InstanceType<typeof Button>
+export type ButtonGroupInstance = InstanceType<typeof ButtonGroup>

+ 58 - 0
packages/components/basic/button/src/use-button.ts

@@ -0,0 +1,58 @@
+import { Text, computed, inject, ref, useSlots } from 'vue'
+import {
+  useDisabled,
+  // useFormItem,
+  useGlobalConfig,
+  useSize,
+} from '@kankan-components/hooks'
+import { buttonGroupContextKey } from '@kankan-components/tokens'
+
+import type { SetupContext } from 'vue'
+import type { ButtonEmits, ButtonProps } from './button'
+
+export const useButton = (
+  props: ButtonProps,
+  emit: SetupContext<ButtonEmits>['emit']
+) => {
+  const buttonGroupContext = inject(buttonGroupContextKey, undefined)
+  const globalConfig = useGlobalConfig('button')
+  // const { form } = useFormItem()
+  const _size = useSize(computed(() => buttonGroupContext?.size))
+  const _disabled = useDisabled()
+  const _ref = ref<HTMLButtonElement>()
+  const slots = useSlots()
+
+  const _type = computed(() => props.type || buttonGroupContext?.type || '')
+  const autoInsertSpace = computed(
+    () => props.autoInsertSpace ?? globalConfig.value?.autoInsertSpace ?? false
+  )
+
+  // add space between two characters in Chinese
+  const shouldAddSpace = computed(() => {
+    const defaultSlot = slots.default?.()
+    if (autoInsertSpace.value && defaultSlot?.length === 1) {
+      const slot = defaultSlot[0]
+      if (slot?.type === Text) {
+        const text = slot.children as string
+        return /^\p{Unified_Ideograph}{2}$/u.test(text.trim())
+      }
+    }
+    return false
+  })
+
+  const handleClick = (evt: MouseEvent) => {
+    // if (props.nativeType === 'reset') {
+    //   form?.resetFields()
+    // }
+    emit('click', evt)
+  }
+
+  return {
+    _disabled,
+    _size,
+    _type,
+    _ref,
+    shouldAddSpace,
+    handleClick,
+  }
+}

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

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

+ 7 - 7
packages/components/basic/config-provider/src/config-provider.ts

@@ -1,11 +1,11 @@
 import { defineComponent, renderSlot, watch } from 'vue'
 import { buildProps, definePropType } from '@kankan-components/utils'
-import { provideGlobalConfig } from '@kankan-components/hooks'
+import { provideGlobalConfig, useSizeProp } from '@kankan-components/hooks'
 
 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 { ButtonConfigContext } from '@kankan-components/components/basic/button'
 // import type { MessageConfigContext } from '@kankan-components/components/message'
 
 // export const messageConfig: MessageConfigContext = {}
@@ -22,11 +22,11 @@ export const configProviderProps = buildProps({
   //     type: definePropType<Language>(Object),
   // },
 
-  // size: useSizeProp,
+  size: useSizeProp,
 
-  // button: {
-  //     type: definePropType<ButtonConfigContext>(Object),
-  // },
+  button: {
+    type: definePropType<ButtonConfigContext>(Object),
+  },
 
   // experimentalFeatures: {
   //     type: definePropType<ExperimentalFeatures>(Object),
@@ -46,7 +46,7 @@ export const configProviderProps = buildProps({
 
   namespace: {
     type: String,
-    default: 'ui',
+    default: 'kk',
   },
 } as const)
 export type ConfigProviderProps = ExtractPropTypes<typeof configProviderProps>

+ 0 - 345
packages/components/basic/dialog/__tests__/dialog.test.tsx

@@ -1,345 +0,0 @@
-import { markRaw, nextTick } from 'vue'
-import { mount } from '@vue/test-utils'
-import { describe, expect, test, vi } from 'vitest'
-import { rAF } from '@element-plus/test-utils/tick'
-import triggerCompositeClick from '@element-plus/test-utils/composite-click'
-import { Delete } from '@element-plus/icons-vue'
-import Dialog from '../src/dialog.vue'
-
-const AXIOM = 'Rem is the best girl'
-
-describe('Dialog.vue', () => {
-  test('render test', async () => {
-    const wrapper = mount(<Dialog modelValue={true}>{AXIOM}</Dialog>)
-
-    await nextTick()
-    await rAF()
-    await nextTick()
-    expect(wrapper.find('.el-dialog__body').text()).toEqual(AXIOM)
-  })
-
-  test('dialog should have a title and header when it has been given', async () => {
-    const HEADER = 'I am header'
-    const wrapper = mount(
-      <Dialog
-        modelValue={true}
-        v-slots={{
-          header: () => HEADER,
-        }}
-      >
-        {AXIOM}
-      </Dialog>
-    )
-
-    await nextTick()
-    expect(wrapper.find('.el-dialog__header').text()).toBe(HEADER)
-
-    mount(
-      <Dialog modelValue={true} title={HEADER}>
-        {AXIOM}
-      </Dialog>
-    )
-    await nextTick()
-
-    expect(wrapper.find('.el-dialog__header').text()).toBe(HEADER)
-  })
-
-  test('dialog header should have slot props', async () => {
-    const wrapper = mount(
-      <Dialog
-        modelValue={true}
-        v-slots={{
-          header: ({
-            titleId,
-            titleClass,
-            close,
-          }: {
-            titleId: string
-            titleClass: string
-            close: () => void
-          }) => (
-            <button
-              data-title-id={titleId}
-              data-title-class={titleClass}
-              onClick={close}
-            />
-          ),
-        }}
-      >
-        {AXIOM}
-      </Dialog>
-    )
-
-    await nextTick()
-    const headerButton = wrapper.find('button')
-    expect(headerButton.attributes()['data-title-id']).toBeTruthy()
-    expect(headerButton.attributes()['data-title-class']).toBe(
-      'el-dialog__title'
-    )
-    expect(wrapper.emitted().close).toBeFalsy()
-    headerButton.trigger('click')
-    await nextTick()
-    expect(wrapper.emitted()).toHaveProperty('close')
-  })
-
-  test('dialog should have a footer when footer has been given', async () => {
-    const wrapper = mount(
-      <Dialog modelValue={true} v-slots={{ footer: () => AXIOM }}>
-        {AXIOM}
-      </Dialog>
-    )
-
-    await nextTick()
-    expect(wrapper.find('.el-dialog__footer').exists()).toBe(true)
-    expect(wrapper.find('.el-dialog__footer').text()).toBe(AXIOM)
-  })
-
-  test('should append dialog to body when appendToBody is true', async () => {
-    const wrapper = mount(
-      <Dialog modelValue={true} appendToBody={true}>
-        {AXIOM}
-      </Dialog>
-    )
-
-    await nextTick()
-    expect(
-      document.body.firstElementChild!.classList.contains('el-overlay')
-    ).toBe(true)
-    wrapper.unmount()
-  })
-
-  test('should center dialog', async () => {
-    const wrapper = mount(
-      <Dialog modelValue={true} center={true}>
-        {AXIOM}
-      </Dialog>
-    )
-
-    await nextTick()
-    expect(wrapper.find('.el-dialog--center').exists()).toBe(true)
-  })
-
-  test('should show close button', async () => {
-    const wrapper = mount(<Dialog modelValue={true}>{AXIOM}</Dialog>)
-
-    await nextTick()
-    expect(wrapper.find('.el-dialog__close').exists()).toBe(true)
-  })
-
-  test('should hide close button when showClose = false', async () => {
-    const wrapper = mount(
-      <Dialog modelValue={true} showClose={false}>
-        {AXIOM}
-      </Dialog>
-    )
-
-    await nextTick()
-    expect(wrapper.find('.el-dialog__headerbtn').exists()).toBe(false)
-  })
-
-  test('should close dialog when click on close button', async () => {
-    const wrapper = mount(<Dialog modelValue={true}>{AXIOM}</Dialog>)
-
-    await nextTick()
-    await wrapper.find('.el-dialog__headerbtn').trigger('click')
-    expect(wrapper.vm.visible).toBe(false)
-  })
-
-  describe('mask related', () => {
-    test('should not have overlay mask when mask is false', async () => {
-      const wrapper = mount(
-        <Dialog modal={false} modelValue={true}>
-          {AXIOM}
-        </Dialog>
-      )
-
-      await nextTick()
-      expect(wrapper.find('.el-overlay').exists()).toBe(false)
-    })
-
-    test('should close the modal when clicking on mask when `closeOnClickModal` is true', async () => {
-      const wrapper = mount(<Dialog modelValue={true}>{AXIOM}</Dialog>)
-
-      await nextTick()
-      expect(wrapper.find('.el-overlay').exists()).toBe(true)
-      expect(wrapper.find('.el-overlay-dialog').exists()).toBe(true)
-
-      await triggerCompositeClick(wrapper.find('.el-overlay-dialog'))
-      expect(wrapper.vm.visible).toBe(false)
-    })
-  })
-
-  describe('life cycles', () => {
-    test('should call before close', async () => {
-      const beforeClose = vi.fn()
-      const wrapper = mount(
-        <Dialog modelValue={true} beforeClose={beforeClose}>
-          {AXIOM}
-        </Dialog>
-      )
-
-      await nextTick()
-      await wrapper.find('.el-dialog__headerbtn').trigger('click')
-      expect(beforeClose).toHaveBeenCalled()
-    })
-
-    test('should not close dialog when user cancelled', async () => {
-      const beforeClose = vi
-        .fn()
-        .mockImplementation((hide: (cancel: boolean) => void) => hide(true))
-
-      const wrapper = mount(
-        <Dialog modelValue={true} beforeClose={beforeClose}>
-          {AXIOM}
-        </Dialog>
-      )
-      await nextTick()
-      await wrapper.find('.el-dialog__headerbtn').trigger('click')
-      expect(beforeClose).toHaveBeenCalled()
-      expect(wrapper.vm.visible).toBe(true)
-    })
-
-    test('should open and close with delay', async () => {
-      const wrapper = mount(
-        <Dialog openDelay={200} closeDelay={200} modelValue={false}>
-          {AXIOM}
-        </Dialog>
-      )
-
-      expect(wrapper.vm.visible).toBe(false)
-
-      await wrapper.setProps({
-        modelValue: true,
-      })
-    })
-
-    test('should destroy on close', async () => {
-      const wrapper = mount(
-        <Dialog modelValue={true} destroyOnClose={true}>
-          {AXIOM}
-        </Dialog>
-      )
-      expect(wrapper.vm.visible).toBe(true)
-      await nextTick()
-      await rAF()
-      await nextTick()
-      await wrapper.find('.el-dialog__headerbtn').trigger('click')
-      await wrapper.setProps({
-        // manually setting this prop because that Transition is not available in testing,
-        // updating model value event was emitted via transition hooks.
-        modelValue: false,
-      })
-      await nextTick()
-      await rAF()
-      await nextTick()
-      expect(wrapper.find('.el-dialog__body').exists()).toBe(false)
-    })
-
-    test('should emit close event', async () => {
-      let visible = true
-      const onClose = vi.fn()
-      const onClosed = vi.fn()
-      const wrapper = mount(
-        <Dialog
-          modelValue={true}
-          onUpdate:modelValue={(val: boolean) => (visible = val)}
-          onClose={onClose}
-          onClosed={onClosed}
-        >
-          {AXIOM}
-        </Dialog>
-      )
-
-      expect(wrapper.vm.visible).toBe(true)
-      await nextTick()
-      await rAF()
-      await nextTick()
-
-      await triggerCompositeClick(wrapper.find('.el-overlay-dialog'))
-      await nextTick()
-      await rAF()
-      await nextTick()
-      expect(onClose).toHaveBeenCalled()
-      expect(onClosed).toHaveBeenCalled()
-      expect(visible).toBe(false)
-    })
-
-    test('closeIcon', async () => {
-      const wrapper = mount(
-        <Dialog modelValue={true} closeIcon={markRaw(Delete)}>
-          {AXIOM}
-        </Dialog>
-      )
-      await nextTick()
-      await rAF()
-      const closeIcon = wrapper.find('svg')
-      expect(closeIcon.exists()).toBe(true)
-      const svg = mount(Delete).find('svg').element
-      expect(closeIcon.element.innerHTML).toBe(svg.innerHTML)
-    })
-
-    test('should render draggable prop', async () => {
-      const wrapper = mount(
-        <Dialog modelValue={true} draggable={true}>
-          {AXIOM}
-        </Dialog>
-      )
-
-      await nextTick()
-      await rAF()
-      await nextTick()
-      expect(wrapper.find('.is-draggable').exists()).toBe(true)
-    })
-  })
-
-  describe('accessibility', () => {
-    test('title attribute should set aria-label', async () => {
-      const title = 'Hello World'
-      const wrapper = mount(
-        <Dialog modelValue={true} title={title}>
-          {AXIOM}
-        </Dialog>
-      )
-      await nextTick()
-      const dialog = wrapper.find('[role="dialog"]')
-      expect(dialog.attributes()['aria-label']).toBe(title)
-      expect(dialog.attributes()['aria-labelledby']).toBeFalsy()
-    })
-
-    test('missing title attribute should point to header slot content', async () => {
-      const wrapper = mount(
-        <Dialog
-          modelValue={true}
-          v-slots={{
-            header: ({
-              titleId,
-              titleClass,
-            }: {
-              titleId: string
-              titleClass: string
-            }) => <h5 id={titleId} class={titleClass} />,
-          }}
-        >
-          {AXIOM}
-        </Dialog>
-      )
-      await nextTick()
-      const dialog = wrapper.find('[role="dialog"]')
-      const dialogTitle = wrapper.find('.el-dialog__title')
-      expect(dialog.attributes()['aria-label']).toBeFalsy()
-      expect(dialog.attributes()['aria-labelledby']).toBe(
-        dialogTitle.attributes().id
-      )
-    })
-
-    test('aria-describedby should point to modal body', async () => {
-      const wrapper = mount(<Dialog modelValue={true}>{AXIOM}</Dialog>)
-      await nextTick()
-      const dialog = wrapper.find('[role="dialog"]')
-      const dialogBody = wrapper.find('.el-dialog__body')
-      expect(dialog.attributes()['aria-describedby']).toBe(
-        dialogBody.attributes().id
-      )
-    })
-  })
-})

+ 3 - 3
packages/components/basic/dialog/index.ts

@@ -1,8 +1,8 @@
-import { withInstall } from '@element-plus/utils'
+import { withInstall } from '@kankan-components/utils'
 import Dialog from './src/dialog.vue'
 
-export const ElDialog = withInstall(Dialog)
-export default ElDialog
+export const UiDialog = withInstall(Dialog)
+export default UiDialog
 
 export * from './src/use-dialog'
 export * from './src/dialog'

+ 4 - 4
packages/components/basic/dialog/src/dialog-content.ts

@@ -1,4 +1,4 @@
-import { buildProps, iconPropType } from '@kankan-components/utils'
+import { buildProps } from '@kankan-components/utils'
 
 export const dialogContentProps = buildProps({
   center: {
@@ -9,9 +9,9 @@ export const dialogContentProps = buildProps({
     type: Boolean,
     default: false,
   },
-  closeIcon: {
-    type: iconPropType,
-  },
+  // closeIcon: {
+  //   type: iconPropType,
+  // },
   customClass: {
     type: String,
     default: '',

+ 10 - 11
packages/components/basic/dialog/src/dialog-content.vue

@@ -20,14 +20,13 @@
       </slot>
       <button
         v-if="showClose"
-        :aria-label="t('el.dialog.close')"
         :class="ns.e('headerbtn')"
         type="button"
         @click="$emit('close')"
       >
-        <ui-icon :class="ns.e('close')">
-          <component :is="closeIcon || Close" />
-        </ui-icon>
+        <kk-icon :class="ns.e('close')" type="close">
+          <!-- <component :is="closeIcon" /> -->
+        </kk-icon>
       </button>
     </header>
     <div :id="bodyId" :class="ns.e('body')">
@@ -41,17 +40,17 @@
 
 <script lang="ts" setup>
 import { computed, inject } from 'vue'
-import { UiIcon } from '@kankan-components/components/basic/icon'
-import { FOCUS_TRAP_INJECTION_KEY } from '@kankan-components/components/focus-trap'
-import { useDraggable, useLocale } from '@kankan-components/hooks'
-import { CloseComponents, composeRefs } from '@kankan-components/utils'
+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 { dialogContentEmits, dialogContentProps } from './dialog-content'
 
-const { t } = useLocale()
-const { Close } = CloseComponents
+// const { t } = useLocale()
+// const { Close } = CloseComponents
 
-defineOptions({ name: 'ElDialogContent' })
+defineOptions({ name: 'UiDialogContent' })
 const props = defineProps(dialogContentProps)
 defineEmits(dialogContentEmits)
 

+ 6 - 7
packages/components/basic/dialog/src/dialog.vue

@@ -25,7 +25,7 @@
           @mousedown="overlayEvent.onMousedown"
           @mouseup="overlayEvent.onMouseup"
         >
-          <el-focus-trap
+          <ui-focus-trap
             loop
             :trapped="visible"
             focus-start-el="container"
@@ -34,14 +34,13 @@
             @focusout-prevented="onFocusoutPrevented"
             @release-requested="onCloseRequested"
           >
-            <el-dialog-content
+            <ui-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"
@@ -62,8 +61,8 @@
               <template v-if="$slots.footer" #footer>
                 <slot name="footer" />
               </template>
-            </el-dialog-content>
-          </el-focus-trap>
+            </ui-dialog-content>
+          </ui-focus-trap>
         </div>
       </ui-overlay>
     </transition>
@@ -75,8 +74,8 @@ import { computed, provide, ref } from 'vue'
 import { UiOverlay } from '@kankan-components/components/basic/overlay'
 import { useNamespace, useSameTarget } from '@kankan-components/hooks'
 import { dialogInjectionKey } from '@kankan-components/tokens'
-import ElFocusTrap from '@element-plus/components/focus-trap'
-import ElDialogContent from './dialog-content.vue'
+import UiFocusTrap from '@kankan-components/components/basic/focus-trap'
+import UiDialogContent from './dialog-content.vue'
 import { dialogEmits, dialogProps } from './dialog'
 import { useDialog } from './use-dialog'
 

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

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

+ 3 - 3
packages/components/basic/dialog/style/index.ts

@@ -1,3 +1,3 @@
-import '@element-plus/components/base/style'
-import '@element-plus/theme-chalk/src/dialog.scss'
-import '@element-plus/components/overlay/style'
+import '@kankan-components/components/base/style'
+import '@kankan-components/theme-chalk/src/dialog.scss'
+import '@kankan-components/components/overlay/style'

+ 354 - 0
packages/components/basic/focus-trap/__tests__/focus-trap.test.ts

@@ -0,0 +1,354 @@
+import { h, inject, nextTick } from 'vue'
+import { mount } from '@vue/test-utils'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { EVENT_CODE } from '@element-plus/constants'
+import ElFocusTrap from '../src/focus-trap.vue'
+import { FOCUS_TRAP_INJECTION_KEY } from '../src/tokens'
+
+const AXIOM = 'rem is the best girl'
+
+describe('<ElFocusTrap', () => {
+  const childKls = 'child-class'
+  const TrapChild = {
+    props: {
+      items: Number,
+    },
+    setup() {
+      const { focusTrapRef, onKeydown } = inject(
+        FOCUS_TRAP_INJECTION_KEY,
+        undefined
+      )!
+      return {
+        focusTrapRef,
+        onKeydown,
+      }
+    },
+    template: `
+      <span class="before-trap" tabindex="0"></span>
+      <div ref="focusTrapRef" tabindex="0" class="focus-container ${childKls}" @keydown="onKeydown">
+        <template v-if="!items">${AXIOM}</template>
+        <template v-else v-for="i in items">
+          <span class="item" tabindex="0">{{ i }}</span>
+        </template>
+      </div>
+      <span class="after-trap" tabindex="0"></span>
+    `,
+  }
+
+  const createComponent = (props = {}, items = 0) =>
+    mount(ElFocusTrap, {
+      props: {
+        trapped: true,
+        ...props,
+      },
+      slots: {
+        default: () => h(TrapChild, { items }),
+      },
+      attachTo: document.body,
+    })
+
+  let wrapper: ReturnType<typeof createComponent>
+  const findFocusContainer = () => wrapper.find('.focus-container')
+  const findDescendants = () => wrapper.findAll('.item')
+  const findBeforeTrap = () => wrapper.find('.before-trap')
+  const findAfterTrap = () => wrapper.find('.after-trap')
+
+  afterEach(() => {
+    wrapper?.unmount()
+    document.body.innerHTML = ''
+  })
+
+  describe('render', () => {
+    it('should render correctly', async () => {
+      wrapper = createComponent()
+      await nextTick()
+      await nextTick()
+
+      const child = findFocusContainer()
+      expect(child.exists()).toBe(true)
+      expect(child.text()).toBe(AXIOM)
+      expect(document.activeElement).toBe(child.element)
+    })
+
+    it('should be able to focus on the first descendant item', async () => {
+      wrapper = createComponent(undefined, 3)
+      await nextTick()
+      await nextTick()
+
+      const descendants = findDescendants()
+      expect(descendants).toHaveLength(3)
+      expect(document.activeElement).toBe(descendants.at(0)?.element)
+    })
+  })
+
+  describe('events', () => {
+    it('should be able to dispatch focus on mount event', async () => {
+      const focusOnMount = vi.fn()
+      wrapper = createComponent({
+        onFocusAfterTrapped: focusOnMount,
+      })
+      await nextTick()
+
+      expect(focusOnMount).toHaveBeenCalled()
+    })
+
+    it('should be able to dispatch focus on unmount', async () => {
+      const focusOnUnmount = vi.fn()
+      wrapper = createComponent({
+        onFocusAfterReleased: focusOnUnmount,
+      })
+      await nextTick()
+      await nextTick()
+      const child = findFocusContainer()
+      expect(document.activeElement).toBe(child.element)
+
+      wrapper.unmount()
+      expect(focusOnUnmount).toHaveBeenCalled()
+      expect(document.activeElement).toBe(document.body)
+    })
+
+    it('should be able to dispatch `release-requested` if escape key pressed while trapped', async () => {
+      wrapper = createComponent(
+        {
+          trapped: false,
+          loop: true,
+        },
+        1
+      )
+      await nextTick()
+
+      const focusContainer = findFocusContainer()
+
+      focusContainer?.trigger('keydown', {
+        key: EVENT_CODE.esc,
+      })
+
+      await nextTick()
+      await nextTick()
+
+      expect(wrapper.emitted('release-requested')).toBeFalsy()
+
+      await wrapper.setProps({ trapped: true })
+
+      await nextTick()
+      await nextTick()
+
+      const items = findDescendants()
+      const firstItem = items.at(0)
+      expect(document.activeElement).toBe(firstItem?.element)
+
+      // Expect no emit if esc while not trapped
+      expect(wrapper.emitted('release-requested')).toBeFalsy()
+
+      focusContainer?.trigger('keydown', {
+        key: EVENT_CODE.esc,
+      })
+
+      await nextTick()
+      await nextTick()
+
+      // Expect emit if esc while trapped
+      expect(wrapper.emitted('release-requested')?.length).toBe(1)
+
+      createComponent({ loop: true }, 3)
+      await nextTick()
+      await nextTick()
+
+      focusContainer?.trigger('keydown', {
+        key: EVENT_CODE.esc,
+      })
+
+      // Expect no emit if esc while layer paused
+      expect(wrapper.emitted('release-requested')?.length).toBe(1)
+    })
+
+    it('should be able to dispatch `focusout-prevented` when trab wraps due to trapped or is blocked', async () => {
+      wrapper = createComponent(undefined, 3)
+      await nextTick()
+      await nextTick()
+
+      const childComponent = findFocusContainer()
+      const items = findDescendants()
+      expect(document.activeElement).toBe(items.at(0)?.element)
+
+      expect(wrapper.emitted('focusout-prevented')).toBeFalsy()
+      await childComponent.trigger('keydown.shift', {
+        key: EVENT_CODE.tab,
+      })
+      expect(document.activeElement).toBe(items.at(0)?.element)
+      expect(wrapper.emitted('focusout-prevented')?.length).toBe(2)
+      ;(items.at(2)?.element as HTMLElement).focus()
+      await childComponent.trigger('keydown', {
+        key: EVENT_CODE.tab,
+      })
+      expect(wrapper.emitted('focusout-prevented')?.length).toBe(4)
+    })
+  })
+
+  describe('features', () => {
+    it('should be able to navigate via keyboard', async () => {
+      wrapper = createComponent(undefined, 3)
+      await nextTick()
+      await nextTick()
+
+      const childComponent = findFocusContainer()
+      const items = findDescendants()
+      expect(document.activeElement).toBe(items.at(0)?.element)
+
+      /**
+       * NOTE:
+       * JSDOM does not support keyboard navigation simulation so that
+       * dispatching keyboard event with tab key is useless, we cannot test it
+       * it here, maybe turn to cypress for robust e2e test would be a better idea
+       */
+      // when loop is off
+      await childComponent.trigger('keydown.shift', {
+        key: EVENT_CODE.tab,
+      })
+      expect(document.activeElement).toBe(items.at(0)?.element)
+      ;(items.at(2)?.element as HTMLElement).focus()
+      expect(document.activeElement).toBe(items.at(2)?.element)
+
+      await childComponent.trigger('keydown', {
+        key: EVENT_CODE.tab,
+      })
+      expect(document.activeElement).toBe(items.at(2)?.element)
+
+      // set loop to true so that tab can tabbing from last to first and back forth
+      await wrapper.setProps({
+        loop: true,
+      })
+
+      await childComponent.trigger('keydown', {
+        key: EVENT_CODE.tab,
+      })
+      expect(document.activeElement).toBe(items.at(0)?.element)
+
+      await childComponent.trigger('keydown.shift', {
+        key: EVENT_CODE.tab,
+      })
+      expect(document.activeElement).toBe(items.at(2)?.element)
+    })
+
+    it('should not be able to navigate when no focusable element contained', async () => {
+      wrapper = createComponent()
+      await nextTick()
+      await nextTick()
+
+      const focusComponent = findFocusContainer()
+      expect(document.activeElement).toBe(focusComponent.element)
+
+      await focusComponent.trigger('keydown', {
+        key: EVENT_CODE.tab,
+      })
+      expect(document.activeElement).toBe(focusComponent.element)
+    })
+
+    it('should be able to navigate outside by keyboard when not trapped', async () => {
+      wrapper = createComponent(
+        {
+          trapped: false,
+        },
+        1
+      )
+      await nextTick()
+      await nextTick()
+
+      let isDefaultPrevented = false
+      const preventDefault = () => {
+        isDefaultPrevented = true
+      }
+
+      const focusContainer = findFocusContainer()
+      const items = findDescendants()
+      const beforeTrap = findBeforeTrap()
+      const afterTrap = findAfterTrap()
+
+      ;(beforeTrap.element as HTMLElement).focus()
+      expect(document.activeElement).toBe(beforeTrap.element)
+
+      await focusContainer.trigger('keydown', {
+        key: EVENT_CODE.tab,
+        preventDefault,
+      })
+      if (!isDefaultPrevented) {
+        ;(items.at(0)?.element as HTMLElement).focus()
+      }
+
+      expect(document.activeElement).toBe(items.at(0)?.element)
+
+      await focusContainer.trigger('keydown', {
+        key: EVENT_CODE.tab,
+        preventDefault,
+      })
+      if (!isDefaultPrevented) {
+        ;(afterTrap.element as HTMLElement).focus()
+      }
+
+      expect(document.activeElement).toBe(afterTrap.element)
+    })
+
+    it('should not be able to navigate if the current layer is paused', async () => {
+      wrapper = createComponent(
+        {
+          loop: true,
+        },
+        3
+      )
+      await nextTick()
+      await nextTick()
+
+      const focusComponent = findFocusContainer()
+      const items = findDescendants()
+      expect(document.activeElement).toBe(items.at(0)?.element)
+
+      await focusComponent.trigger('keydown.shift', {
+        key: EVENT_CODE.tab,
+      })
+      expect(document.activeElement).toBe(items.at(2)?.element)
+
+      const newFocusTrap = createComponent({ loop: true }, 3)
+      await nextTick()
+      await nextTick()
+      expect(document.activeElement).toBe(newFocusTrap.find('.item').element)
+
+      await focusComponent.trigger('keydown', {
+        key: EVENT_CODE.tab,
+      })
+      expect(document.activeElement).not.toBe(items.at(0)?.element)
+      newFocusTrap.unmount()
+      await nextTick()
+
+      expect(document.activeElement).toBe(items.at(2)?.element)
+
+      await focusComponent.trigger('keydown', {
+        key: EVENT_CODE.tab,
+      })
+      expect(document.activeElement).toBe(items.at(0)?.element)
+    })
+
+    it('should steal focus when trapped', async () => {
+      wrapper = createComponent(
+        {
+          trapped: false,
+          loop: true,
+        },
+        1
+      )
+      await nextTick()
+
+      const beforeTrap = findBeforeTrap()
+      ;(beforeTrap.element as HTMLElement).focus()
+      expect(document.activeElement).toBe(beforeTrap.element)
+
+      await wrapper.setProps({ trapped: true })
+
+      await nextTick()
+      await nextTick()
+
+      const items = findDescendants()
+      const firstItem = items.at(0)
+      expect(document.activeElement).toBe(firstItem?.element)
+    })
+  })
+})

+ 56 - 0
packages/components/basic/focus-trap/__tests__/utils.test.ts

@@ -0,0 +1,56 @@
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import {
+  focusFirstDescendant,
+  getEdges,
+  obtainAllFocusableElements,
+} from '../src/utils'
+
+describe('focus-trap utils', () => {
+  const buildDOM = () => {
+    const parser = new DOMParser()
+    return parser.parseFromString(
+      `
+    <div class="root">
+      <span>some val</span>
+      <input class="focusable-input" />
+      <span tabindex="0" class="focusable-span">other val</span>
+      <span hidden tabindex="0"> hidden span</span>
+      <input disabled />
+      <input hidden />
+    </div>
+    `,
+      'text/html'
+    )
+  }
+
+  beforeEach(() => {
+    const dom = buildDOM()
+
+    Array.from(dom.children).forEach((child) => {
+      document.body.appendChild(child)
+    })
+  })
+
+  afterEach(() => {
+    document.body.innerHTML = ''
+  })
+
+  it('should be able to focus', () => {
+    const focusable = obtainAllFocusableElements(document.body)
+    expect(focusable).toHaveLength(2)
+    expect(focusable[0].classList.contains('focusable-input')).toBeTruthy()
+  })
+
+  it('should be able to get focusable edge', () => {
+    const [first, last] = getEdges(document.body)
+    expect(first?.classList.contains('focusable-input')).toBeTruthy()
+    expect(last?.classList.contains('focusable-span')).toBeTruthy()
+  })
+
+  it('should be able to focus on the first descendant', () => {
+    expect(document.activeElement).toBe(document.body)
+    const focusable = obtainAllFocusableElements(document.body)
+    focusFirstDescendant(focusable)
+    expect(document.activeElement).toBe(focusable[0])
+  })
+})

+ 7 - 0
packages/components/basic/focus-trap/index.ts

@@ -0,0 +1,7 @@
+import UiFocusTrap from './src/focus-trap.vue'
+
+export { UiFocusTrap }
+
+export default UiFocusTrap
+export * from './src/tokens'
+export * from './src/utils'

+ 328 - 0
packages/components/basic/focus-trap/src/focus-trap.vue

@@ -0,0 +1,328 @@
+<template>
+  <slot :handle-keydown="onKeydown" />
+</template>
+<script lang="ts">
+import {
+  defineComponent,
+  nextTick,
+  onBeforeUnmount,
+  onMounted,
+  provide,
+  ref,
+  unref,
+  watch,
+} from 'vue'
+import { isNil } from 'lodash-unified'
+import { EVENT_CODE } from '@kankan-components/constants'
+import { useEscapeKeydown } from '@kankan-components/hooks'
+import { isString } from '@kankan-components/utils'
+import {
+  createFocusOutPreventedEvent,
+  focusFirstDescendant,
+  focusableStack,
+  getEdges,
+  isFocusCausedByUserEvent,
+  obtainAllFocusableElements,
+  tryFocus,
+  useFocusReason,
+} from './utils'
+import {
+  FOCUS_AFTER_RELEASED,
+  FOCUS_AFTER_TRAPPED,
+  FOCUS_AFTER_TRAPPED_OPTS,
+  FOCUS_TRAP_INJECTION_KEY,
+  ON_RELEASE_FOCUS_EVT,
+  ON_TRAP_FOCUS_EVT,
+} from './tokens'
+
+import type { PropType } from 'vue'
+import type { FocusLayer } from './utils'
+
+export default defineComponent({
+  name: 'UiFocusTrap',
+  inheritAttrs: false,
+  props: {
+    loop: Boolean,
+    trapped: Boolean,
+    focusTrapEl: Object as PropType<HTMLElement>,
+    focusStartEl: {
+      type: [Object, String] as PropType<'container' | 'first' | HTMLElement>,
+      default: 'first',
+    },
+  },
+  emits: [
+    ON_TRAP_FOCUS_EVT,
+    ON_RELEASE_FOCUS_EVT,
+    'focusin',
+    'focusout',
+    'focusout-prevented',
+    'release-requested',
+  ],
+  setup(props, { emit }) {
+    const forwardRef = ref<HTMLElement | undefined>()
+    let lastFocusBeforeTrapped: HTMLElement | null
+    let lastFocusAfterTrapped: HTMLElement | null
+
+    const { focusReason } = useFocusReason()
+
+    useEscapeKeydown((event) => {
+      if (props.trapped && !focusLayer.paused) {
+        emit('release-requested', event)
+      }
+    })
+
+    const focusLayer: FocusLayer = {
+      paused: false,
+      pause() {
+        this.paused = true
+      },
+      resume() {
+        this.paused = false
+      },
+    }
+
+    const onKeydown = (e: KeyboardEvent) => {
+      if (!props.loop && !props.trapped) return
+      if (focusLayer.paused) return
+
+      const { key, altKey, ctrlKey, metaKey, currentTarget, shiftKey } = e
+      const { loop } = props
+      const isTabbing =
+        key === EVENT_CODE.tab && !altKey && !ctrlKey && !metaKey
+
+      const currentFocusingEl = document.activeElement
+      if (isTabbing && currentFocusingEl) {
+        const container = currentTarget as HTMLElement
+        const [first, last] = getEdges(container)
+        const isTabbable = first && last
+        if (!isTabbable) {
+          if (currentFocusingEl === container) {
+            const focusoutPreventedEvent = createFocusOutPreventedEvent({
+              focusReason: focusReason.value,
+            })
+            emit('focusout-prevented', focusoutPreventedEvent)
+            if (!focusoutPreventedEvent.defaultPrevented) {
+              e.preventDefault()
+            }
+          }
+        } else {
+          if (!shiftKey && currentFocusingEl === last) {
+            const focusoutPreventedEvent = createFocusOutPreventedEvent({
+              focusReason: focusReason.value,
+            })
+            emit('focusout-prevented', focusoutPreventedEvent)
+            if (!focusoutPreventedEvent.defaultPrevented) {
+              e.preventDefault()
+              if (loop) tryFocus(first, true)
+            }
+          } else if (
+            shiftKey &&
+            [first, container].includes(currentFocusingEl as HTMLElement)
+          ) {
+            const focusoutPreventedEvent = createFocusOutPreventedEvent({
+              focusReason: focusReason.value,
+            })
+            emit('focusout-prevented', focusoutPreventedEvent)
+            if (!focusoutPreventedEvent.defaultPrevented) {
+              e.preventDefault()
+              if (loop) tryFocus(last, true)
+            }
+          }
+        }
+      }
+    }
+
+    provide(FOCUS_TRAP_INJECTION_KEY, {
+      focusTrapRef: forwardRef,
+      onKeydown,
+    })
+
+    watch(
+      () => props.focusTrapEl,
+      (focusTrapEl) => {
+        if (focusTrapEl) {
+          forwardRef.value = focusTrapEl
+        }
+      },
+      { immediate: true }
+    )
+
+    watch([forwardRef], ([forwardRef], [oldForwardRef]) => {
+      if (forwardRef) {
+        forwardRef.addEventListener('keydown', onKeydown)
+        forwardRef.addEventListener('focusin', onFocusIn)
+        forwardRef.addEventListener('focusout', onFocusOut)
+      }
+      if (oldForwardRef) {
+        oldForwardRef.removeEventListener('keydown', onKeydown)
+        oldForwardRef.removeEventListener('focusin', onFocusIn)
+        oldForwardRef.removeEventListener('focusout', onFocusOut)
+      }
+    })
+
+    const trapOnFocus = (e: Event) => {
+      emit(ON_TRAP_FOCUS_EVT, e)
+    }
+    const releaseOnFocus = (e: Event) => emit(ON_RELEASE_FOCUS_EVT, e)
+
+    const onFocusIn = (e: FocusEvent) => {
+      const trapContainer = unref(forwardRef)
+      if (!trapContainer) return
+
+      const target = e.target as HTMLElement | null
+      const relatedTarget = e.relatedTarget as HTMLElement | null
+      const isFocusedInTrap = target && trapContainer.contains(target)
+
+      if (!props.trapped) {
+        const isPrevFocusedInTrap =
+          relatedTarget && trapContainer.contains(relatedTarget)
+        if (!isPrevFocusedInTrap) {
+          lastFocusBeforeTrapped = relatedTarget
+        }
+      }
+
+      if (isFocusedInTrap) emit('focusin', e)
+
+      if (focusLayer.paused) return
+
+      if (props.trapped) {
+        if (isFocusedInTrap) {
+          lastFocusAfterTrapped = target
+        } else {
+          tryFocus(lastFocusAfterTrapped, true)
+        }
+      }
+    }
+
+    const onFocusOut = (e: Event) => {
+      const trapContainer = unref(forwardRef)
+      if (focusLayer.paused || !trapContainer) return
+
+      if (props.trapped) {
+        const relatedTarget = (e as FocusEvent)
+          .relatedTarget as HTMLElement | null
+        if (!isNil(relatedTarget) && !trapContainer.contains(relatedTarget)) {
+          // Give embedded focus layer time to pause this layer before reclaiming focus
+          // And only reclaim focus if it should currently be trapping
+          setTimeout(() => {
+            if (!focusLayer.paused && props.trapped) {
+              const focusoutPreventedEvent = createFocusOutPreventedEvent({
+                focusReason: focusReason.value,
+              })
+              emit('focusout-prevented', focusoutPreventedEvent)
+              if (!focusoutPreventedEvent.defaultPrevented) {
+                tryFocus(lastFocusAfterTrapped, true)
+              }
+            }
+          }, 0)
+        }
+      } else {
+        const target = e.target as HTMLElement | null
+        const isFocusedInTrap = target && trapContainer.contains(target)
+        if (!isFocusedInTrap) emit('focusout', e)
+      }
+    }
+
+    async function startTrap() {
+      // Wait for forwardRef to resolve
+      await nextTick()
+      const trapContainer = unref(forwardRef)
+      if (trapContainer) {
+        focusableStack.push(focusLayer)
+        const prevFocusedElement = trapContainer.contains(
+          document.activeElement
+        )
+          ? lastFocusBeforeTrapped
+          : document.activeElement
+        lastFocusBeforeTrapped = prevFocusedElement as HTMLElement | null
+        const isPrevFocusContained = trapContainer.contains(prevFocusedElement)
+        if (!isPrevFocusContained) {
+          const focusEvent = new Event(
+            FOCUS_AFTER_TRAPPED,
+            FOCUS_AFTER_TRAPPED_OPTS
+          )
+          trapContainer.addEventListener(FOCUS_AFTER_TRAPPED, trapOnFocus)
+          trapContainer.dispatchEvent(focusEvent)
+          if (!focusEvent.defaultPrevented) {
+            nextTick(() => {
+              let focusStartEl = props.focusStartEl
+              if (!isString(focusStartEl)) {
+                tryFocus(focusStartEl)
+                if (document.activeElement !== focusStartEl) {
+                  focusStartEl = 'first'
+                }
+              }
+              if (focusStartEl === 'first') {
+                focusFirstDescendant(
+                  obtainAllFocusableElements(trapContainer),
+                  true
+                )
+              }
+              if (
+                document.activeElement === prevFocusedElement ||
+                focusStartEl === 'container'
+              ) {
+                tryFocus(trapContainer)
+              }
+            })
+          }
+        }
+      }
+    }
+
+    function stopTrap() {
+      const trapContainer = unref(forwardRef)
+
+      if (trapContainer) {
+        trapContainer.removeEventListener(FOCUS_AFTER_TRAPPED, trapOnFocus)
+
+        const releasedEvent = new CustomEvent(FOCUS_AFTER_RELEASED, {
+          ...FOCUS_AFTER_TRAPPED_OPTS,
+          detail: {
+            focusReason: focusReason.value,
+          },
+        })
+        trapContainer.addEventListener(FOCUS_AFTER_RELEASED, releaseOnFocus)
+        trapContainer.dispatchEvent(releasedEvent)
+
+        if (
+          !releasedEvent.defaultPrevented &&
+          (focusReason.value == 'keyboard' || !isFocusCausedByUserEvent())
+        ) {
+          tryFocus(lastFocusBeforeTrapped ?? document.body)
+        }
+
+        trapContainer.removeEventListener(FOCUS_AFTER_RELEASED, trapOnFocus)
+        focusableStack.remove(focusLayer)
+      }
+    }
+
+    onMounted(() => {
+      if (props.trapped) {
+        startTrap()
+      }
+
+      watch(
+        () => props.trapped,
+        (trapped) => {
+          if (trapped) {
+            startTrap()
+          } else {
+            stopTrap()
+          }
+        }
+      )
+    })
+
+    onBeforeUnmount(() => {
+      if (props.trapped) {
+        stopTrap()
+      }
+    })
+
+    return {
+      onKeydown,
+    }
+  },
+})
+</script>

+ 24 - 0
packages/components/basic/focus-trap/src/tokens.ts

@@ -0,0 +1,24 @@
+import type { InjectionKey, Ref } from 'vue'
+
+export const FOCUS_AFTER_TRAPPED = 'focus-trap.focus-after-trapped'
+export const FOCUS_AFTER_RELEASED = 'focus-trap.focus-after-released'
+export const FOCUSOUT_PREVENTED = 'focus-trap.focusout-prevented'
+export const FOCUS_AFTER_TRAPPED_OPTS: EventInit = {
+  cancelable: true,
+  bubbles: false,
+}
+export const FOCUSOUT_PREVENTED_OPTS: EventInit = {
+  cancelable: true,
+  bubbles: false,
+}
+
+export const ON_TRAP_FOCUS_EVT = 'focusAfterTrapped'
+export const ON_RELEASE_FOCUS_EVT = 'focusAfterReleased'
+
+export type FocusTrapInjectionContext = {
+  focusTrapRef: Ref<HTMLElement | undefined>
+  onKeydown: (e: KeyboardEvent) => void
+}
+
+export const FOCUS_TRAP_INJECTION_KEY: InjectionKey<FocusTrapInjectionContext> =
+  Symbol('elFocusTrap')

+ 196 - 0
packages/components/basic/focus-trap/src/utils.ts

@@ -0,0 +1,196 @@
+import { onBeforeUnmount, onMounted, ref } from 'vue'
+import { FOCUSOUT_PREVENTED, FOCUSOUT_PREVENTED_OPTS } from './tokens'
+
+const focusReason = ref<'pointer' | 'keyboard'>()
+const lastUserFocusTimestamp = ref<number>(0)
+const lastAutomatedFocusTimestamp = ref<number>(0)
+let focusReasonUserCount = 0
+
+export type FocusLayer = {
+  paused: boolean
+  pause: () => void
+  resume: () => void
+}
+
+export type FocusStack = FocusLayer[]
+
+export const obtainAllFocusableElements = (
+  element: HTMLElement
+): HTMLElement[] => {
+  const nodes: HTMLElement[] = []
+  const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, {
+    acceptNode: (
+      node: Element & {
+        disabled: boolean
+        hidden: boolean
+        type: string
+        tabIndex: number
+      }
+    ) => {
+      const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden'
+      if (node.disabled || node.hidden || isHiddenInput)
+        return NodeFilter.FILTER_SKIP
+      return node.tabIndex >= 0 || node === document.activeElement
+        ? NodeFilter.FILTER_ACCEPT
+        : NodeFilter.FILTER_SKIP
+    },
+  })
+  while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement)
+
+  return nodes
+}
+
+export const getVisibleElement = (
+  elements: HTMLElement[],
+  container: HTMLElement
+) => {
+  for (const element of elements) {
+    if (!isHidden(element, container)) return element
+  }
+}
+
+export const isHidden = (element: HTMLElement, container: HTMLElement) => {
+  if (process.env.NODE_ENV === 'test') return false
+  if (getComputedStyle(element).visibility === 'hidden') return true
+
+  while (element) {
+    if (container && element === container) return false
+    if (getComputedStyle(element).display === 'none') return true
+    element = element.parentElement as HTMLElement
+  }
+
+  return false
+}
+
+export const getEdges = (container: HTMLElement) => {
+  const focusable = obtainAllFocusableElements(container)
+  const first = getVisibleElement(focusable, container)
+  const last = getVisibleElement(focusable.reverse(), container)
+  return [first, last]
+}
+
+const isSelectable = (
+  element: any
+): element is HTMLInputElement & { select: () => void } => {
+  return element instanceof HTMLInputElement && 'select' in element
+}
+
+export const tryFocus = (
+  element?: HTMLElement | { focus: () => void } | null,
+  shouldSelect?: boolean
+) => {
+  if (element && element.focus) {
+    const prevFocusedElement = document.activeElement
+    element.focus({ preventScroll: true })
+    lastAutomatedFocusTimestamp.value = window.performance.now()
+    if (
+      element !== prevFocusedElement &&
+      isSelectable(element) &&
+      shouldSelect
+    ) {
+      element.select()
+    }
+  }
+}
+
+function removeFromStack<T>(list: T[], item: T) {
+  const copy = [...list]
+
+  const idx = list.indexOf(item)
+
+  if (idx !== -1) {
+    copy.splice(idx, 1)
+  }
+  return copy
+}
+
+const createFocusableStack = () => {
+  let stack = [] as FocusStack
+
+  const push = (layer: FocusLayer) => {
+    const currentLayer = stack[0]
+
+    if (currentLayer && layer !== currentLayer) {
+      currentLayer.pause()
+    }
+
+    stack = removeFromStack(stack, layer)
+    stack.unshift(layer)
+  }
+
+  const remove = (layer: FocusLayer) => {
+    stack = removeFromStack(stack, layer)
+    stack[0]?.resume?.()
+  }
+
+  return {
+    push,
+    remove,
+  }
+}
+
+export const focusFirstDescendant = (
+  elements: HTMLElement[],
+  shouldSelect = false
+) => {
+  const prevFocusedElement = document.activeElement
+  for (const element of elements) {
+    tryFocus(element, shouldSelect)
+    if (document.activeElement !== prevFocusedElement) return
+  }
+}
+
+export const focusableStack = createFocusableStack()
+
+export const isFocusCausedByUserEvent = (): boolean => {
+  return lastUserFocusTimestamp.value > lastAutomatedFocusTimestamp.value
+}
+
+const notifyFocusReasonPointer = () => {
+  focusReason.value = 'pointer'
+  lastUserFocusTimestamp.value = window.performance.now()
+}
+
+const notifyFocusReasonKeydown = () => {
+  focusReason.value = 'keyboard'
+  lastUserFocusTimestamp.value = window.performance.now()
+}
+
+export const useFocusReason = (): {
+  focusReason: typeof focusReason
+  lastUserFocusTimestamp: typeof lastUserFocusTimestamp
+  lastAutomatedFocusTimestamp: typeof lastAutomatedFocusTimestamp
+} => {
+  onMounted(() => {
+    if (focusReasonUserCount === 0) {
+      document.addEventListener('mousedown', notifyFocusReasonPointer)
+      document.addEventListener('touchstart', notifyFocusReasonPointer)
+      document.addEventListener('keydown', notifyFocusReasonKeydown)
+    }
+    focusReasonUserCount++
+  })
+
+  onBeforeUnmount(() => {
+    focusReasonUserCount--
+    if (focusReasonUserCount <= 0) {
+      document.removeEventListener('mousedown', notifyFocusReasonPointer)
+      document.removeEventListener('touchstart', notifyFocusReasonPointer)
+      document.removeEventListener('keydown', notifyFocusReasonKeydown)
+    }
+  })
+
+  return {
+    focusReason,
+    lastUserFocusTimestamp,
+    lastAutomatedFocusTimestamp,
+  }
+}
+
+export const createFocusOutPreventedEvent = (
+  detail: CustomEventInit['detail']
+) => {
+  return new CustomEvent(FOCUSOUT_PREVENTED, {
+    ...FOCUSOUT_PREVENTED_OPTS,
+    detail,
+  })
+}

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

@@ -2,8 +2,8 @@ import { withInstall } from '@kankan-components/utils'
 
 import Icon from './src/icon.vue'
 
-export const UiIcon = withInstall(Icon)
+export const KkIcon = withInstall(Icon)
 
-export default UiIcon
+export default KkIcon
 
 export * from './src/icon'

+ 4 - 2
packages/components/basic/icon/src/icon.vue

@@ -3,10 +3,11 @@
     class="iconfont ui-kankan-icon icon"
     :class="className"
     :style="style"
+    v-bind="$attrs"
     @click="(ev) => emit('click', ev)"
   >
     <slot />
-    <p v-if="tip && os.isPc && !os.isTablet" class="tip">{{ tip }}</p>
+    <p v-if="tip && os?.isPc && !os?.isTablet" class="tip">{{ tip }}</p>
   </i>
 </template>
 
@@ -16,13 +17,14 @@ import { normalizeUnitToStyle, os } from '@kankan-components/utils'
 import { iconProps } from './icon'
 
 defineOptions({
-  name: 'UIIcon',
+  name: 'KkIcon',
 })
 
 const props = defineProps(iconProps)
 
 const style = computed(() => ({
   'font-size': normalizeUnitToStyle(props.size || 14),
+  'min-width': normalizeUnitToStyle(props.size || 14),
   color: props.color,
 }))
 const className = computed(() => {

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

@@ -1,3 +1,7 @@
 export * from './icon'
 export * from './button'
 export * from './audio'
+export * from './overlay'
+export * from './dialog'
+export * from './config-provider'
+export * from './focus-trap'

+ 2 - 2
packages/components/basic/overlay/style/css.ts

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

+ 2 - 2
packages/components/basic/overlay/style/index.ts

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

+ 5 - 0
packages/hooks/index.ts

@@ -3,3 +3,8 @@ export * from './use-global-config'
 export * from './use-namespace'
 export * from './use-same-target'
 export * from './use-id'
+export * from './use-draggable'
+export * from './use-escape-keydown'
+export * from './use-lockscreen'
+export * from './use-escape-keydown'
+export * from './use-common-props'

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

@@ -0,0 +1,46 @@
+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)
+}

+ 87 - 0
packages/hooks/use-draggable/index.ts

@@ -0,0 +1,87 @@
+import { onBeforeUnmount, onMounted, watchEffect } from 'vue'
+import { addUnit } from '@kankan-components/utils'
+import type { ComputedRef, Ref } from 'vue'
+
+export const useDraggable = (
+  targetRef: Ref<HTMLElement | undefined>,
+  dragRef: Ref<HTMLElement | undefined>,
+  draggable: ComputedRef<boolean>
+) => {
+  let transform = {
+    offsetX: 0,
+    offsetY: 0,
+  }
+
+  const onMousedown = (e: MouseEvent) => {
+    const downX = e.clientX
+    const downY = e.clientY
+    const { offsetX, offsetY } = transform
+
+    const targetRect = targetRef.value!.getBoundingClientRect()
+    const targetLeft = targetRect.left
+    const targetTop = targetRect.top
+    const targetWidth = targetRect.width
+    const targetHeight = targetRect.height
+
+    const clientWidth = document.documentElement.clientWidth
+    const clientHeight = document.documentElement.clientHeight
+
+    const minLeft = -targetLeft + offsetX
+    const minTop = -targetTop + offsetY
+    const maxLeft = clientWidth - targetLeft - targetWidth + offsetX
+    const maxTop = clientHeight - targetTop - targetHeight + offsetY
+
+    const onMousemove = (e: MouseEvent) => {
+      const moveX = Math.min(
+        Math.max(offsetX + e.clientX - downX, minLeft),
+        maxLeft
+      )
+      const moveY = Math.min(
+        Math.max(offsetY + e.clientY - downY, minTop),
+        maxTop
+      )
+
+      transform = {
+        offsetX: moveX,
+        offsetY: moveY,
+      }
+      targetRef.value!.style.transform = `translate(${addUnit(
+        moveX
+      )}, ${addUnit(moveY)})`
+    }
+
+    const onMouseup = () => {
+      document.removeEventListener('mousemove', onMousemove)
+      document.removeEventListener('mouseup', onMouseup)
+    }
+
+    document.addEventListener('mousemove', onMousemove)
+    document.addEventListener('mouseup', onMouseup)
+  }
+
+  const onDraggable = () => {
+    if (dragRef.value && targetRef.value) {
+      dragRef.value.addEventListener('mousedown', onMousedown)
+    }
+  }
+
+  const offDraggable = () => {
+    if (dragRef.value && targetRef.value) {
+      dragRef.value.removeEventListener('mousedown', onMousedown)
+    }
+  }
+
+  onMounted(() => {
+    watchEffect(() => {
+      if (draggable.value) {
+        onDraggable()
+      } else {
+        offDraggable()
+      }
+    })
+  })
+
+  onBeforeUnmount(() => {
+    offDraggable()
+  })
+}

+ 32 - 0
packages/hooks/use-escape-keydown/index.ts

@@ -0,0 +1,32 @@
+import { onBeforeUnmount, onMounted } from 'vue'
+import { isClient } from '@vueuse/core'
+import { EVENT_CODE } from '@kankan-components/constants'
+
+let registeredEscapeHandlers: ((e: KeyboardEvent) => void)[] = []
+
+const cachedHandler = (e: Event) => {
+  const event = e as KeyboardEvent
+  if (event.key === EVENT_CODE.esc) {
+    registeredEscapeHandlers.forEach((registeredHandler) =>
+      registeredHandler(event)
+    )
+  }
+}
+
+export const useEscapeKeydown = (handler: (e: KeyboardEvent) => void) => {
+  onMounted(() => {
+    if (registeredEscapeHandlers.length === 0) {
+      document.addEventListener('keydown', cachedHandler)
+    }
+    if (isClient) registeredEscapeHandlers.push(handler)
+  })
+
+  onBeforeUnmount(() => {
+    registeredEscapeHandlers = registeredEscapeHandlers.filter(
+      (registeredHandler) => registeredHandler !== handler
+    )
+    if (registeredEscapeHandlers.length === 0) {
+      if (isClient) document.removeEventListener('keydown', cachedHandler)
+    }
+  })
+}

+ 73 - 0
packages/hooks/use-lockscreen/index.ts

@@ -0,0 +1,73 @@
+import { isRef, onScopeDispose, watch } from 'vue'
+import { computed } from '@vue/reactivity'
+import { isClient } from '@vueuse/core'
+import {
+  addClass,
+  getScrollBarWidth,
+  getStyle,
+  hasClass,
+  removeClass,
+  throwError,
+} from '@kankan-components/utils'
+import { useNamespace } from '../use-namespace'
+
+import type { Ref } from 'vue'
+
+/**
+ * Hook that monitoring the ref value to lock or unlock the screen.
+ * When the trigger became true, it assumes modal is now opened and vice versa.
+ * @param trigger {Ref<boolean>}
+ */
+export const useLockscreen = (trigger: Ref<boolean>) => {
+  if (!isRef(trigger)) {
+    throwError(
+      '[useLockscreen]',
+      'You need to pass a ref param to this function'
+    )
+  }
+
+  const ns = useNamespace('popup')
+
+  const hiddenCls = computed(() => ns.bm('parent', 'hidden'))
+
+  if (!isClient || hasClass(document.body, hiddenCls.value)) {
+    return
+  }
+
+  let scrollBarWidth = 0
+  let withoutHiddenClass = false
+  let bodyWidth = '0'
+
+  const cleanup = () => {
+    setTimeout(() => {
+      removeClass(document.body, hiddenCls.value)
+      if (withoutHiddenClass) {
+        document.body.style.width = bodyWidth
+      }
+    }, 200)
+  }
+  watch(trigger, (val) => {
+    if (!val) {
+      cleanup()
+      return
+    }
+
+    withoutHiddenClass = !hasClass(document.body, hiddenCls.value)
+    if (withoutHiddenClass) {
+      bodyWidth = document.body.style.width
+    }
+    scrollBarWidth = getScrollBarWidth(ns.namespace.value)
+    const bodyHasOverflow =
+      document.documentElement.clientHeight < document.body.scrollHeight
+    const bodyOverflowY = getStyle(document.body, 'overflowY')
+    if (
+      scrollBarWidth > 0 &&
+      (bodyHasOverflow || bodyOverflowY === 'scroll') &&
+      withoutHiddenClass
+    ) {
+      document.body.style.width = `calc(100% - ${scrollBarWidth}px)`
+    }
+    addClass(document.body, hiddenCls.value)
+  })
+  onScopeDispose(() => cleanup())
+}

+ 58 - 0
packages/hooks/use-teleport/index.ts

@@ -0,0 +1,58 @@
+import { Teleport, h, onUnmounted, ref } from 'vue'
+import { NOOP } from '@vue/shared'
+import { isClient } from '@vueuse/core'
+import { createGlobalNode, removeGlobalNode } from '@kankan-components/utils'
+
+import type { Ref, VNode } from 'vue'
+
+export const useTeleport = (
+  contentRenderer: () => VNode,
+  appendToBody: Ref<boolean>
+) => {
+  const isTeleportVisible = ref(false)
+
+  if (!isClient) {
+    return {
+      isTeleportVisible,
+      showTeleport: NOOP,
+      hideTeleport: NOOP,
+      renderTeleport: NOOP,
+    }
+  }
+
+  let $el: HTMLElement | null = null
+
+  const showTeleport = () => {
+    isTeleportVisible.value = true
+    // this allows the delayed showing strategy since the the content itself could be enterable
+    // e.g. el-popper
+    if ($el !== null) return
+
+    $el = createGlobalNode()
+  }
+
+  const hideTeleport = () => {
+    isTeleportVisible.value = false
+    if ($el !== null) {
+      removeGlobalNode($el)
+      $el = null
+    }
+  }
+
+  const renderTeleport = () => {
+    return appendToBody.value !== true
+      ? contentRenderer()
+      : isTeleportVisible.value
+      ? [h(Teleport, { to: $el }, contentRenderer())]
+      : undefined
+  }
+
+  onUnmounted(hideTeleport)
+
+  return {
+    isTeleportVisible,
+    showTeleport,
+    hideTeleport,
+    renderTeleport,
+  }
+}

+ 4 - 3
packages/kankan-components/component.ts

@@ -1,9 +1,10 @@
-import { UIIcon } from '@kankan-components/components/basic/icon'
-import { UIButton } from '@kankan-components/components/basic/button'
+import { KkIcon } from '@kankan-components/components/basic/icon'
+import { KkButton } from '@kankan-components/components/basic/button'
 import { UIAudio } from '@kankan-components/components/basic/audio'
+import { UiDialog } from '@kankan-components/components/basic/dialog'
 
 import { UITag } from '@kankan-components/components/advance/tag'
 
 import type { Plugin } from 'vue'
 
-export default [UIIcon, UIButton, UIAudio, UITag] as Plugin[]
+export default [KkIcon, KkButton, UIAudio, UiDialog, UITag] as Plugin[]

+ 3 - 3
packages/kankan-components/index.ts

@@ -1,9 +1,9 @@
 import installer from './defaults'
 export * from '@kankan-components/components'
-// export * from '@element-plus/constants'
-// export * from '@element-plus/directives'
+export * from '@kankan-components/constants'
+// export * from '@kankan-components/directives'
 export * from '@kankan-components/hooks'
-// export * from '@element-plus/tokens'
+export * from '@kankan-components/tokens'
 export * from './make-installer'
 
 export const install = installer.install

+ 1 - 1
packages/theme-chalk/gulpfile.ts

@@ -35,7 +35,7 @@ function buildThemeChalk() {
     .pipe(
       rename((path) => {
         if (!noElPrefixFile.test(path.basename)) {
-          path.basename = `ui-${path.basename}`
+          path.basename = `kk-${path.basename}`
         }
       })
     )

+ 1 - 1
packages/theme-chalk/src/base.scss

@@ -1,3 +1,3 @@
 @use 'var.scss';
-@use '../common/base.scss';
+@use 'common/transition.scss';
 @use 'icon.scss';

+ 284 - 59
packages/theme-chalk/src/button.scss

@@ -1,79 +1,304 @@
 @use 'sass:map';
 
-.ui-button {
-    width: 100%;
-    height: 34px;
-    border: none;
+@use 'common/var' as *;
+@use 'mixins/button' as *;
+@use 'mixins/mixins' as *;
+@use 'mixins/utils' as *;
+@use 'mixins/var' as *;
+
+$button-icon-span-gap: () !default;
+$button-icon-span-gap: map.merge(
+  (
+    'large': 8px,
+    'default': 6px,
+    'small': 4px,
+  ),
+  $button-icon-span-gap
+);
+
+@include b(button) {
+  @include set-component-css-var('button', $button);
+}
+
+@include b(button) {
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+
+  line-height: 1;
+  // min-height will expand when in flex
+  height: map.get($input-height, 'default');
+  white-space: nowrap;
+  cursor: pointer;
+  color: getCssVar('button', 'text-color');
+  text-align: center;
+  box-sizing: border-box;
+  outline: none;
+  transition: 0.1s;
+  font-weight: getCssVar('button', 'font-weight');
+  user-select: none;
+  vertical-align: middle;
+  -webkit-appearance: none;
+  background-color: getCssVar('button', 'bg-color');
+  border: getCssVar('border');
+  border-color: getCssVar('button', 'border-color');
+
+  &:hover,
+  &:focus {
+    color: getCssVar('button', 'hover', 'text-color');
+    border-color: getCssVar('button', 'hover', 'border-color');
+    background-color: getCssVar('button', 'hover', 'bg-color');
     outline: none;
-    border-radius: 4px;
-    font-size: 14px;
-    background: none !important;
+  }
+
+  &:active {
+    color: getCssVar('button', 'active', 'text-color');
+    border-color: getCssVar('button', 'active', 'border-color');
+    background-color: getCssVar('button', 'active', 'bg-color');
+    outline: none;
+  }
+
+  &:focus-visible {
+    outline: 2px solid getCssVar('button', 'outline-color');
+    outline-offset: 1px;
+  }
+
+  > span {
+    display: inline-flex;
+    align-items: center;
+  }
+
+  & + & {
+    margin-left: 12px;
+  }
+
+  @include button-size(
+    map.get($button-padding-vertical, 'default') - $button-border-width,
+    map.get($button-padding-horizontal, 'default') - $button-border-width,
+    map.get($button-font-size, 'default'),
+    map.get($button-border-radius, 'default')
+  );
 
-    transition: all 0.3s ease;
+  &::-moz-focus-inner {
+    border: 0;
+  }
 
-    .ui-button-icon {
-        margin-right: 0.6em;
+  & [class*='#{$namespace}-icon'] {
+    & + span {
+      margin-left: map.get($button-icon-span-gap, 'default');
     }
-}
+    svg {
+      vertical-align: bottom;
+    }
+  }
 
-.ui-button.customize {
-    background: none;
-    color: rgba(var(--color), 0.8);
-    border: 1px solid rgba(var(--color), 0.6);
-}
+  @include when(plain) {
+    @include css-var-from-global(
+      ('button', 'hover', 'text-color'),
+      ('color', 'primary')
+    );
+    @include css-var-from-global(
+      ('button', 'hover', 'bg-color'),
+      ('fill-color', 'blank')
+    );
+    @include css-var-from-global(
+      ('button', 'hover', 'border-color'),
+      ('color', 'primary')
+    );
+  }
+
+  @include when(active) {
+    color: getCssVar('button', 'active', 'text-color');
+    border-color: getCssVar('button', 'active', 'border-color');
+    background-color: getCssVar('button', 'active', 'bg-color');
+    outline: none;
+  }
+
+  @include when(disabled) {
+    &,
+    &:hover,
+    &:focus {
+      color: getCssVar('button', 'disabled', 'text-color');
+      cursor: not-allowed;
+      background-image: none;
+      background-color: getCssVar('button', 'disabled', 'bg-color');
+      border-color: getCssVar('button', 'disabled', 'border-color');
+    }
+  }
+
+  @include when(loading) {
+    position: relative;
+    pointer-events: none;
 
-.ui-button.normal {
-    color: var(--colors-color);
-    border: 1px solid var(--colors-normal-base);
-    &:hover {
-        color: var(--colors-normal-hover);
-        border: 1px solid var(--colors-normal-hover);
+    &:before {
+      // mask the button
+      z-index: 1;
+      pointer-events: none;
+      content: '';
+      position: absolute;
+      left: -1px;
+      top: -1px;
+      right: -1px;
+      bottom: -1px;
+      border-radius: inherit;
+      background-color: getCssVar('mask-color', 'extra-light');
     }
-    &:active {
-        color: var(--colors-normal-click);
-        border: 1px solid var(--colors-normal-click);
+  }
+  @include when(round) {
+    border-radius: getCssVar('border-radius', 'round');
+  }
+  @include when(circle) {
+    border-radius: 50%;
+    padding: map.get($button-padding-vertical, 'default') - $button-border-width;
+  }
+
+  @include when(text) {
+    color: getCssVar('button', 'text-color');
+    border: 0 solid transparent;
+    background-color: transparent;
+
+    @include when(disabled) {
+      color: getCssVar('button', 'disabled', 'text-color');
+      background-color: transparent !important;
     }
-}
 
-.ui-button.submit {
-    color: var(--color-main-hover);
-    border: 1px solid var(--color-main-normal);
-    background-color: var(--color-main-normal);
-    &:hover {
-        border-color: var(--color-main-hover);
-        background-color: var(--color-main-hover);
+    &:not(.is-disabled) {
+      &:hover,
+      &:focus {
+        background-color: getCssVar('fill-color', 'light');
+      }
+
+      &:focus-visible {
+        outline: 2px solid getCssVar('button', 'outline-color');
+        outline-offset: 1px;
+      }
+
+      &:active {
+        background-color: getCssVar('fill-color');
+      }
+
+      @include when(has-bg) {
+        background-color: getCssVar('fill-color', 'light');
+
+        &:hover,
+        &:focus {
+          background-color: getCssVar('fill-color');
+        }
+
+        &:active {
+          background-color: getCssVar('fill-color', 'dark');
+        }
+      }
     }
-    &:active {
-        border-color: var(--color-main-focus);
-        background-color: var(--color-main-focus);
+  }
+
+  @include e(text) {
+    @include m(expand) {
+      letter-spacing: 0.3em;
+      margin-right: -0.3em;
     }
-}
+  }
 
-.ui-button.cancel {
-    color: var(--color-main-normal);
-    border: 1px solid var(--color-main-normal);
-    &:hover {
-        border-color: var(--color-main-hover);
+  @include when(link) {
+    border-color: transparent;
+    color: getCssVar('button', 'text-color');
+    background: transparent;
+    padding: 2px;
+    height: auto;
+
+    &:hover,
+    &:focus {
+      color: getCssVar('button', 'hover', 'link-text-color');
     }
-    &:active {
-        border-color: var(--color-main-focus);
+
+    @include when(disabled) {
+      color: getCssVar('button', 'disabled', 'text-color');
+      background-color: transparent !important;
+      border-color: transparent !important;
     }
-}
 
-.ui-button.primary {
-    background-color: var(--colors-primary-base) !important;
-    color: var(--colors-normal-fill-hover);
-    border: none;
-    opacity: 1;
-
-    // &:active,
-    &:hover {
-        // opacity: 0.8;
-        // background: var(--colors-primary-hover) !important;
-        background: #4dd8c7 !important;
+    &:not(.is-disabled) {
+      &:hover,
+      &:focus {
+        border-color: transparent;
+        background-color: transparent;
+      }
+
+      &:active {
+        color: getCssVar('button', 'active-color');
+        border-color: transparent;
+        background-color: transparent;
+      }
+    }
+  }
+
+  @include m(text) {
+    border-color: transparent;
+    background: transparent;
+    color: getCssVar('color', 'primary');
+    padding-left: 0;
+    padding-right: 0;
+    @include when(disabled) {
+      color: getCssVar('button', 'disabled', 'text-color');
+      background-color: transparent !important;
+      border-color: transparent !important;
     }
-    &:active {
-        background-color: var(--colors-primary-active) !important;
-        color: rgba(255, 255, 255, 0.6);
+
+    &:not(.is-disabled) {
+      &:hover,
+      &:focus {
+        color: getCssVar('color', 'primary', 'light-3');
+        border-color: transparent;
+        background-color: transparent;
+      }
+
+      &:active {
+        color: getCssVar('color', 'primary', 'dark-2');
+        border-color: transparent;
+        background-color: transparent;
+      }
+    }
+  }
+
+  @include e(link) {
+    @include m(expand) {
+      letter-spacing: 0.3em;
+      margin-right: -0.3em;
+    }
+  }
+
+  @each $type in (primary, success, warning, danger, info) {
+    @include m($type) {
+      @include button-variant($type);
+    }
+  }
+
+  @each $size in (large, small) {
+    @include m($size) {
+      @include set-css-var-value(
+        ('button', 'size'),
+        map.get($input-height, $size)
+      );
+
+      height: getCssVar('button', 'size');
+
+      & [class*='#{$namespace}-icon'] {
+        & + span {
+          margin-left: map.get($button-icon-span-gap, $size);
+        }
+      }
+
+      @include button-size(
+        map.get($button-padding-vertical, $size) - $button-border-width,
+        map.get($button-padding-horizontal, $size) - $button-border-width,
+        map.get($button-font-size, $size),
+        map.get($button-border-radius, $size)
+      );
+
+      @include when(circle) {
+        width: getCssVar('button', 'size');
+        padding: map.get($button-padding-vertical, $size) - $button-border-width;
+      }
     }
+  }
 }

+ 17 - 0
packages/theme-chalk/src/color/index.scss

@@ -0,0 +1,17 @@
+@use 'sass:color';
+@use 'sass:string';
+
+@function rgb2hex($color) {
+  @return unquote('#' + #{string.slice(color.ie-hex-str($color), 4)});
+}
+
+// rgba color above solid color
+@function mix-overlay-color($upper, $lower) {
+  $opacity: color.alpha($upper);
+
+  $red: color.red($upper) * $opacity + color.red($lower) * (1 - $opacity);
+  $green: color.green($upper) * $opacity + color.green($lower) * (1 - $opacity);
+  $blue: color.blue($upper) * $opacity + color.blue($lower) * (1 - $opacity);
+
+  @return rgb2hex(rgb($red, $green, $blue));
+}

+ 60 - 0
packages/theme-chalk/src/common/_var.scss.bk

@@ -0,0 +1,60 @@
+:root {
+  --colors-primary-fill: 255, 255, 255;
+  --colors-primary-base-fill: 0, 200, 175;
+  --colors-primary-base: rgb(var(--colors-primary-base-fill));
+  --colors-primary-hover: #4dd8c7;
+  // --colors-primary-hover: #008B7A;
+  --colors-primary-active: #008b7a;
+  --colors-primary-click: #005046;
+  --colors-warn: #fa3f48;
+  --colors-color: #999;
+  --colors-border-color: rgba(var(--colors-primary-fill), 0.16);
+  --colors-content-color: rgb(--colors-primary-fill);
+
+  --colors-normal-back: rgba(var(--colors-primary-fill), 0.1);
+  --colors-normal-base: rgba(var(--colors-primary-fill), 0.4);
+  --colors-normal-hover: rgba(var(--colors-primary-fill), 1);
+  --colors-normal-click: var(--colors-primary-click);
+
+  --colors-normal-fill-back: var(--colors-normal-back);
+  --colors-normal-fill-base: var(--colors-normal-base);
+  --colors-normal-fill-hover: var(--colors-normal-hover);
+  --colors-normal-fill-click: var(--colors-primary-click);
+
+  --colors-error-fill: 250, 63, 72;
+
+  --small-size: 12px;
+  --medium-size: 14px;
+  --big-size: 16px;
+
+  // 正常
+  --color-main-normal: var(--colors-primary-base);
+  // 悬停
+  --color-main-hover: var(--colors-primary-hover);
+  // 点击
+  --color-main-focus: var(--colors-primary-click);
+
+  --editor-head-filter: blur(0px);
+  --editor-head-height: 50px;
+
+  --editor-head-back: rgba(20, 20, 20, 0.86);
+
+  --editor-menu-filter: var(--editor-head-filter);
+  --editor-menu-width: 80px;
+  --editor-menu-left: 0px;
+  --editor-menu-right: 0px;
+  --editer-menu-fill: 27, 27, 28;
+  --editor-menu-back: rgba(var(--editer-menu-fill), 0.8);
+  --editor-menu-active-back: rgba(var(--colors-primary-fill), 0.06);
+  --editor-menu-color: #999;
+  --editor-menu-active: rgba(255, 255, 255, 0.06);
+
+  --editor-toolbox-top: var(--editor-head-height);
+  --editor-toolbox-left: calc(
+    var(--editor-menu-left) + var(--editor-menu-width)
+  );
+  --editor-toolbox-width: 340px;
+  --editor-toolbox-back: var(--editor-menu-back);
+  --editor-toolbox-padding: 20px;
+  --editor-toolbar-height: 60px;
+}

+ 47 - 0
packages/theme-chalk/src/common/popup.scss

@@ -0,0 +1,47 @@
+@use './var' as *;
+@use '../mixins/mixins' as *;
+@use '../mixins/var' as *;
+
+:root {
+  @include set-component-css-var('popup', $popup);
+}
+
+.v-modal-enter {
+  animation: v-modal-in getCssVar('transition-duration-fast') ease;
+}
+
+.v-modal-leave {
+  animation: v-modal-out getCssVar('transition-duration-fast') ease forwards;
+}
+
+@keyframes v-modal-in {
+  0% {
+    opacity: 0;
+  }
+  100% {
+  }
+}
+
+@keyframes v-modal-out {
+  0% {
+  }
+  100% {
+    opacity: 0;
+  }
+}
+
+.v-modal {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  opacity: getCssVar('popup-modal-opacity');
+  background: getCssVar('popup-modal-bg-color');
+}
+
+@include b(popup-parent) {
+  @include m(hidden) {
+    overflow: hidden;
+  }
+}

+ 122 - 0
packages/theme-chalk/src/common/transition.scss

@@ -0,0 +1,122 @@
+@use '../mixins/config' as *;
+@use '../mixins/mixins' as *;
+
+.fade-in-linear-enter-active,
+.fade-in-linear-leave-active {
+  transition: getCssVar('transition-fade', 'linear');
+}
+
+.fade-in-linear-enter-from,
+.fade-in-linear-leave-to {
+  opacity: 0;
+}
+
+.#{$namespace}-fade-in-linear-enter-active,
+.#{$namespace}-fade-in-linear-leave-active {
+  transition: getCssVar('transition-fade', 'linear');
+}
+.#{$namespace}-fade-in-linear-enter-from,
+.#{$namespace}-fade-in-linear-leave-to {
+  opacity: 0;
+}
+
+.#{$namespace}-fade-in-enter-active,
+.#{$namespace}-fade-in-leave-active {
+  transition: all getCssVar('transition-duration') cubic-bezier(0.55, 0, 0.1, 1);
+}
+.#{$namespace}-fade-in-enter-from,
+.#{$namespace}-fade-in-leave-active {
+  opacity: 0;
+}
+
+.#{$namespace}-zoom-in-center-enter-active,
+.#{$namespace}-zoom-in-center-leave-active {
+  transition: all getCssVar('transition-duration') cubic-bezier(0.55, 0, 0.1, 1);
+}
+.#{$namespace}-zoom-in-center-enter-from,
+.#{$namespace}-zoom-in-center-leave-active {
+  opacity: 0;
+  transform: scaleX(0);
+}
+
+.#{$namespace}-zoom-in-top-enter-active,
+.#{$namespace}-zoom-in-top-leave-active {
+  opacity: 1;
+  transform: scaleY(1);
+  transition: getCssVar('transition-md-fade');
+  transform-origin: center top;
+
+  &[data-popper-placement^='top'] {
+    transform-origin: center bottom;
+  }
+}
+.#{$namespace}-zoom-in-top-enter-from,
+.#{$namespace}-zoom-in-top-leave-active {
+  opacity: 0;
+  transform: scaleY(0);
+}
+
+.#{$namespace}-zoom-in-bottom-enter-active,
+.#{$namespace}-zoom-in-bottom-leave-active {
+  opacity: 1;
+  transform: scaleY(1);
+  transition: getCssVar('transition-md-fade');
+  transform-origin: center bottom;
+}
+.#{$namespace}-zoom-in-bottom-enter-from,
+.#{$namespace}-zoom-in-bottom-leave-active {
+  opacity: 0;
+  transform: scaleY(0);
+}
+
+.#{$namespace}-zoom-in-left-enter-active,
+.#{$namespace}-zoom-in-left-leave-active {
+  opacity: 1;
+  transform: scale(1, 1);
+  transition: getCssVar('transition-md-fade');
+  transform-origin: top left;
+}
+.#{$namespace}-zoom-in-left-enter-from,
+.#{$namespace}-zoom-in-left-leave-active {
+  opacity: 0;
+  transform: scale(0.45, 0.45);
+}
+
+.collapse-transition {
+  transition: getCssVar('transition-duration') height ease-in-out,
+    getCssVar('transition-duration') padding-top ease-in-out,
+    getCssVar('transition-duration') padding-bottom ease-in-out;
+}
+
+.#{$namespace}-collapse-transition-leave-active,
+.#{$namespace}-collapse-transition-enter-active {
+  transition: getCssVar('transition-duration') max-height ease-in-out,
+    getCssVar('transition-duration') padding-top ease-in-out,
+    getCssVar('transition-duration') padding-bottom ease-in-out;
+}
+
+.horizontal-collapse-transition {
+  transition: getCssVar('transition-duration') width ease-in-out,
+    getCssVar('transition-duration') padding-left ease-in-out,
+    getCssVar('transition-duration') padding-right ease-in-out;
+}
+
+.#{$namespace}-list-enter-active,
+.#{$namespace}-list-leave-active {
+  transition: all 1s;
+}
+
+.#{$namespace}-list-enter-from,
+.#{$namespace}-list-leave-to {
+  opacity: 0;
+  transform: translateY(-30px);
+}
+
+.#{$namespace}-list-leave-active {
+  position: absolute !important;
+}
+
+.#{$namespace}-opacity-transition {
+  transition: opacity getCssVar('transition-duration')
+    cubic-bezier(0.55, 0, 0.1, 1);
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1419 - 0
packages/theme-chalk/src/common/var.scss


+ 2 - 1
packages/theme-chalk/src/index.scss

@@ -1,6 +1,7 @@
 @use './base.scss';
+// component styles
 @use './icon.scss';
 @use './button.scss';
 @use './audio.scss';
-
+// component adv styles
 @use './tag.scss';

+ 167 - 0
packages/theme-chalk/src/mixins/_button.scss

@@ -0,0 +1,167 @@
+@use 'sass:map';
+
+@use '../mixins/var' as *;
+@use '../mixins/function' as *;
+@use '../common/var' as *;
+
+@mixin button-plain($type) {
+  $button-color-types: (
+    '': (
+      'text-color': (
+        'color',
+        $type,
+      ),
+      'bg-color': (
+        'color',
+        $type,
+        'light-9',
+      ),
+      'border-color': (
+        'color',
+        $type,
+        'light-5',
+      ),
+    ),
+    'hover': (
+      'text-color': (
+        'color',
+        'white',
+      ),
+      'bg-color': (
+        'color',
+        $type,
+      ),
+      'border-color': (
+        'color',
+        $type,
+      ),
+    ),
+    'active': (
+      'text-color': (
+        'color',
+        'white',
+      ),
+    ),
+  );
+
+  @each $type, $typeMap in $button-color-types {
+    @each $typeColor, $list in $typeMap {
+      @include css-var-from-global(('button', $type, $typeColor), $list);
+    }
+  }
+
+  &.is-disabled {
+    &,
+    &:hover,
+    &:focus,
+    &:active {
+      color: getCssVar('color', $type, 'light-5');
+      background-color: getCssVar('color', $type, 'light-9');
+      border-color: getCssVar('color', $type, 'light-8');
+    }
+  }
+}
+
+@mixin button-variant($type) {
+  $button-color-types: (
+    '': (
+      'text-color': (
+        'color',
+        'white',
+      ),
+      'bg-color': (
+        'color',
+        $type,
+      ),
+      'border-color': (
+        'color',
+        $type,
+      ),
+      'outline-color': (
+        'color',
+        $type,
+        'light-5',
+      ),
+      'active-color': (
+        'color',
+        $type,
+        'dark-2',
+      ),
+    ),
+    'hover': (
+      'text-color': (
+        'color',
+        'white',
+      ),
+      'link-text-color': (
+        'color',
+        $type,
+        'light-5',
+      ),
+      'bg-color': (
+        'color',
+        $type,
+        'light-3',
+      ),
+      'border-color': (
+        'color',
+        $type,
+        'light-3',
+      ),
+    ),
+    'active': (
+      'bg-color': (
+        'color',
+        $type,
+        'dark-2',
+      ),
+      'border-color': (
+        'color',
+        $type,
+        'dark-2',
+      ),
+    ),
+    'disabled': (
+      'text-color': (
+        'color',
+        'white',
+      ),
+      'bg-color': (
+        'color',
+        $type,
+        'light-5',
+      ),
+      'border-color': (
+        'color',
+        $type,
+        'light-5',
+      ),
+    ),
+  );
+
+  @each $type, $typeMap in $button-color-types {
+    @each $typeColor, $list in $typeMap {
+      @include css-var-from-global(('button', $type, $typeColor), $list);
+    }
+  }
+
+  &.is-plain,
+  &.is-text,
+  &.is-link {
+    @include button-plain($type);
+  }
+}
+
+@mixin button-size(
+  $padding-vertical,
+  $padding-horizontal,
+  $font-size,
+  $border-radius
+) {
+  padding: $padding-vertical $padding-horizontal;
+  font-size: $font-size;
+  border-radius: $border-radius;
+  &.is-round {
+    padding: $padding-vertical $padding-horizontal;
+  }
+}

+ 38 - 0
packages/theme-chalk/src/mixins/_col.scss

@@ -0,0 +1,38 @@
+@use 'sass:math';
+
+@use '../common/var' as *;
+@use './mixins' as *;
+
+@mixin col-size($size) {
+  @include res($size) {
+    .#{$namespace}-col-#{$size}-0 {
+      display: none;
+      @include when(guttered) {
+        display: none;
+      }
+    }
+    @for $i from 0 through 24 {
+      .#{$namespace}-col-#{$size}-#{$i} {
+        @if $i != 0 {
+          display: block;
+        }
+        max-width: (math.div(1, 24) * $i * 100) * 1%;
+        flex: 0 0 (math.div(1, 24) * $i * 100) * 1%;
+      }
+
+      .#{$namespace}-col-#{$size}-offset-#{$i} {
+        margin-left: (math.div(1, 24) * $i * 100) * 1%;
+      }
+
+      .#{$namespace}-col-#{$size}-pull-#{$i} {
+        position: relative;
+        right: (math.div(1, 24) * $i * 100) * 1%;
+      }
+
+      .#{$namespace}-col-#{$size}-push-#{$i} {
+        position: relative;
+        left: (math.div(1, 24) * $i * 100) * 1%;
+      }
+    }
+  }
+}

+ 66 - 0
packages/theme-chalk/src/mixins/_var.scss

@@ -0,0 +1,66 @@
+@use 'sass:map';
+
+@use 'config';
+@use 'function' as *;
+@use '../common/var' as *;
+
+// set css var value, because we need translate value to string
+// for example:
+// @include set-css-var-value(('color', 'primary'), red);
+// --el-color-primary: red;
+@mixin set-css-var-value($name, $value) {
+  #{joinVarName($name)}: #{$value};
+}
+
+// @include set-css-var-type('color', 'primary', $map);
+// --el-color-primary: #{map.get($map, 'primary')};
+@mixin set-css-var-type($name, $type, $variables) {
+  #{getCssVarName($name, $type)}: #{map.get($variables, $type)};
+}
+
+@mixin set-css-color-type($colors, $type) {
+  @include set-css-var-value(('color', $type), map.get($colors, $type, 'base'));
+
+  @each $i in (3, 5, 7, 8, 9) {
+    @include set-css-var-value(
+      ('color', $type, 'light', $i),
+      map.get($colors, $type, 'light-#{$i}')
+    );
+  }
+
+  @include set-css-var-value(
+    ('color', $type, 'dark-2'),
+    map.get($colors, $type, 'dark-2')
+  );
+}
+
+// set all css var for component by map
+@mixin set-component-css-var($name, $variables) {
+  @each $attribute, $value in $variables {
+    @if $attribute == 'default' {
+      #{getCssVarName($name)}: #{$value};
+    } @else {
+      #{getCssVarName($name, $attribute)}: #{$value};
+    }
+  }
+}
+
+@mixin set-css-color-rgb($type) {
+  $color: map.get($colors, $type, 'base');
+  @include set-css-var-value(
+    ('color', $type, 'rgb'),
+    #{red($color),
+    green($color),
+    blue($color)}
+  );
+}
+
+// generate css var from existing css var
+// for example:
+// @include css-var-from-global(('button', 'text-color'), ('color', $type))
+// --el-button-text-color: var(--el-color-#{$type});
+@mixin css-var-from-global($var, $gVar) {
+  $varName: joinVarName($var);
+  $gVarName: joinVarName($gVar);
+  #{$varName}: var(#{$gVarName});
+}

+ 5 - 0
packages/theme-chalk/src/mixins/config.scss

@@ -0,0 +1,5 @@
+$namespace: 'kk' !default;
+$common-separator: '-' !default;
+$element-separator: '__' !default;
+$modifier-separator: '--' !default;
+$state-prefix: 'is-' !default;

+ 86 - 0
packages/theme-chalk/src/mixins/function.scss

@@ -0,0 +1,86 @@
+@use 'config';
+
+// BEM support Func
+@function selectorToString($selector) {
+  $selector: inspect($selector);
+  $selector: str-slice($selector, 2, -2);
+  @return $selector;
+}
+
+@function containsModifier($selector) {
+  $selector: selectorToString($selector);
+
+  @if str-index($selector, config.$modifier-separator) {
+    @return true;
+  } @else {
+    @return false;
+  }
+}
+
+@function containWhenFlag($selector) {
+  $selector: selectorToString($selector);
+
+  @if str-index($selector, '.' + config.$state-prefix) {
+    @return true;
+  } @else {
+    @return false;
+  }
+}
+
+@function containPseudoClass($selector) {
+  $selector: selectorToString($selector);
+
+  @if str-index($selector, ':') {
+    @return true;
+  } @else {
+    @return false;
+  }
+}
+
+@function hitAllSpecialNestRule($selector) {
+  @return containsModifier($selector) or containWhenFlag($selector) or
+    containPseudoClass($selector);
+}
+
+// join var name
+// joinVarName(('button', 'text-color')) => '--el-button-text-color'
+@function joinVarName($list) {
+  $name: '--' + config.$namespace;
+  @each $item in $list {
+    @if $item != '' {
+      $name: $name + '-' + $item;
+    }
+  }
+  @return $name;
+}
+
+// getCssVarName('button', 'text-color') => '--el-button-text-color'
+@function getCssVarName($args...) {
+  @return joinVarName($args);
+}
+
+// getCssVar('button', 'text-color') => var(--el-button-text-color)
+@function getCssVar($args...) {
+  @return var(#{joinVarName($args)});
+}
+
+// getCssVarWithDefault(('button', 'text-color'), red) => var(--el-button-text-color, red)
+@function getCssVarWithDefault($args, $default) {
+  @return var(#{joinVarName($args)}, #{$default});
+}
+
+// bem('block', 'element', 'modifier') => 'el-block__element--modifier'
+@function bem($block, $element: '', $modifier: '') {
+  $name: config.$namespace + config.$common-separator + $block;
+
+  @if $element != '' {
+    $name: $name + config.$element-separator + $element;
+  }
+
+  @if $modifier != '' {
+    $name: $name + config.$modifier-separator + $modifier;
+  }
+
+  // @debug $name;
+  @return $name;
+}

+ 235 - 0
packages/theme-chalk/src/mixins/mixins.scss

@@ -0,0 +1,235 @@
+@use 'function' as *;
+@use '../common/var' as *;
+// forward mixins
+@forward 'config';
+@forward 'function';
+@forward '_var';
+@use 'config' as *;
+
+// Break-points
+@mixin res($key, $map: $breakpoints) {
+  // loop breakpoint Map, return if present
+  @if map-has-key($map, $key) {
+    @media only screen and #{unquote(map-get($map, $key))} {
+      @content;
+    }
+  } @else {
+    @warn "Undefined points: `#{$map}`";
+  }
+}
+
+// Scrollbar
+@mixin scroll-bar {
+  $scrollbar-thumb-background: getCssVar('text-color', 'disabled');
+  $scrollbar-track-background: getCssVar('fill-color', 'blank');
+
+  &::-webkit-scrollbar {
+    z-index: 11;
+    width: 6px;
+
+    &:horizontal {
+      height: 6px;
+    }
+
+    &-thumb {
+      border-radius: 5px;
+      width: 6px;
+      background: $scrollbar-thumb-background;
+    }
+
+    &-corner {
+      background: $scrollbar-track-background;
+    }
+
+    &-track {
+      background: $scrollbar-track-background;
+
+      &-piece {
+        background: $scrollbar-track-background;
+        width: 6px;
+      }
+    }
+  }
+}
+
+// BEM
+@mixin b($block) {
+  $B: $namespace + '-' + $block !global;
+
+  .#{$B} {
+    @content;
+  }
+}
+
+@mixin e($element) {
+  $E: $element !global;
+  $selector: &;
+  $currentSelector: '';
+  @each $unit in $element {
+    $currentSelector: #{$currentSelector +
+      '.' +
+      $B +
+      $element-separator +
+      $unit +
+      ','};
+  }
+
+  @if hitAllSpecialNestRule($selector) {
+    @at-root {
+      #{$selector} {
+        #{$currentSelector} {
+          @content;
+        }
+      }
+    }
+  } @else {
+    @at-root {
+      #{$currentSelector} {
+        @content;
+      }
+    }
+  }
+}
+
+@mixin m($modifier) {
+  $selector: &;
+  $currentSelector: '';
+  @each $unit in $modifier {
+    $currentSelector: #{$currentSelector +
+      $selector +
+      $modifier-separator +
+      $unit +
+      ','};
+  }
+
+  @at-root {
+    #{$currentSelector} {
+      @content;
+    }
+  }
+}
+
+@mixin configurable-m($modifier, $E-flag: false) {
+  $selector: &;
+  $interpolation: '';
+
+  @if $E-flag {
+    $interpolation: $element-separator + $E-flag;
+  }
+
+  @at-root {
+    #{$selector} {
+      .#{$B + $interpolation + $modifier-separator + $modifier} {
+        @content;
+      }
+    }
+  }
+}
+
+@mixin spec-selector(
+  $specSelector: '',
+  $element: $E,
+  $modifier: false,
+  $block: $B
+) {
+  $modifierCombo: '';
+
+  @if $modifier {
+    $modifierCombo: $modifier-separator + $modifier;
+  }
+
+  @at-root {
+    #{&}#{$specSelector}.#{$block
+      + $element-separator
+      + $element
+      + $modifierCombo} {
+      @content;
+    }
+  }
+}
+
+@mixin meb($modifier: false, $element: $E, $block: $B) {
+  $selector: &;
+  $modifierCombo: '';
+
+  @if $modifier {
+    $modifierCombo: $modifier-separator + $modifier;
+  }
+
+  @at-root {
+    #{$selector} {
+      .#{$block + $element-separator + $element + $modifierCombo} {
+        @content;
+      }
+    }
+  }
+}
+
+@mixin when($state) {
+  @at-root {
+    &.#{$state-prefix + $state} {
+      @content;
+    }
+  }
+}
+
+@mixin extend-rule($name) {
+  @extend #{'%shared-' + $name} !optional;
+}
+
+@mixin share-rule($name) {
+  $rule-name: '%shared-' + $name;
+
+  @at-root #{$rule-name} {
+    @content;
+  }
+}
+
+@mixin pseudo($pseudo) {
+  @at-root #{&}#{':#{$pseudo}'} {
+    @content;
+  }
+}
+
+@mixin picker-popper($background, $border, $box-shadow) {
+  &.#{$namespace}-popper {
+    background: $background;
+    border: $border;
+    box-shadow: $box-shadow;
+
+    .#{$namespace}-popper__arrow {
+      &::before {
+        border: $border;
+      }
+    }
+
+    @each $placement,
+      $adjacency
+        in ('top': 'left', 'bottom': 'right', 'left': 'bottom', 'right': 'top')
+    {
+      &[data-popper-placement^='#{$placement}'] {
+        .#{$namespace}-popper__arrow::before {
+          border-#{$placement}-color: transparent;
+          border-#{$adjacency}-color: transparent;
+        }
+      }
+    }
+  }
+}
+
+// dark
+@mixin dark($block) {
+  html.dark {
+    @include b($block) {
+      @content;
+    }
+  }
+}
+
+@mixin inset-input-border($color, $important: false) {
+  @if $important == true {
+    box-shadow: 0 0 0 1px $color inset !important;
+  } @else {
+    box-shadow: 0 0 0 1px $color inset;
+  }
+}

+ 39 - 0
packages/theme-chalk/src/mixins/utils.scss

@@ -0,0 +1,39 @@
+@mixin utils-clearfix {
+  $selector: &;
+
+  @at-root {
+    #{$selector}::before,
+    #{$selector}::after {
+      display: table;
+      content: '';
+    }
+    #{$selector}::after {
+      clear: both;
+    }
+  }
+}
+
+@mixin utils-vertical-center {
+  $selector: &;
+
+  @at-root {
+    #{$selector}::after {
+      display: inline-block;
+      content: '';
+      height: 100%;
+      vertical-align: middle;
+    }
+  }
+}
+
+@mixin utils-ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+@mixin utils-inline-flex-center {
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+}

+ 92 - 0
packages/theme-chalk/src/reset.scss

@@ -0,0 +1,92 @@
+@use 'common/var' as *;
+@use 'mixins/mixins' as *;
+
+body {
+  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
+    'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
+  font-weight: 400;
+  font-size: getCssVar('font-size', 'base');
+  color: getCssVar('text-color', 'primary');
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-tap-highlight-color: transparent;
+}
+
+a {
+  color: getCssVar('color', 'primary');
+  text-decoration: none;
+
+  &:hover,
+  &:focus {
+    color: getCssVar('color-primary', 'light-3');
+  }
+
+  &:active {
+    color: getCssVar('color-primary', 'dark-2');
+  }
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  color: getCssVar('text-color', 'regular');
+  font-weight: inherit;
+
+  &:first-child {
+    margin-top: 0;
+  }
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+h1 {
+  font-size: calc(getCssVar('font-size', 'base') + 6px);
+}
+
+h2 {
+  font-size: calc(getCssVar('font-size', 'base') + 4px);
+}
+
+h3 {
+  font-size: calc(getCssVar('font-size', 'base') + 2px);
+}
+
+h4,
+h5,
+h6,
+p {
+  font-size: inherit;
+}
+
+p {
+  line-height: 1.8;
+
+  &:first-child {
+    margin-top: 0;
+  }
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+sup,
+sub {
+  font-size: calc(getCssVar('font-size', 'base') - 1px);
+}
+
+small {
+  font-size: calc(getCssVar('font-size', 'base') - 2px);
+}
+
+hr {
+  margin-top: 20px;
+  margin-bottom: 20px;
+  border: 0;
+  border-top: 1px solid getCssVar('border-color', 'lighter');
+}

+ 149 - 56
packages/theme-chalk/src/var.scss

@@ -1,60 +1,153 @@
+// :root {
+//   --colors-primary-fill: 255, 255, 255;
+//   --colors-primary-base-fill: 0, 200, 175;
+//   --colors-primary-base: rgb(var(--colors-primary-base-fill));
+//   --colors-primary-hover: #4dd8c7;
+//   // --colors-primary-hover: #008B7A;
+//   --colors-primary-active: #008b7a;
+//   --colors-primary-click: #005046;
+//   --colors-warn: #fa3f48;
+//   --colors-color: #999;
+//   --colors-border-color: rgba(var(--colors-primary-fill), 0.16);
+//   --colors-content-color: rgb(--colors-primary-fill);
+
+//   --colors-normal-back: rgba(var(--colors-primary-fill), 0.1);
+//   --colors-normal-base: rgba(var(--colors-primary-fill), 0.4);
+//   --colors-normal-hover: rgba(var(--colors-primary-fill), 1);
+//   --colors-normal-click: var(--colors-primary-click);
+
+//   --colors-normal-fill-back: var(--colors-normal-back);
+//   --colors-normal-fill-base: var(--colors-normal-base);
+//   --colors-normal-fill-hover: var(--colors-normal-hover);
+//   --colors-normal-fill-click: var(--colors-primary-click);
+
+//   --colors-error-fill: 250, 63, 72;
+
+//   --small-size: 12px;
+//   --medium-size: 14px;
+//   --big-size: 16px;
+
+//   // 正常
+//   --color-main-normal: var(--colors-primary-base);
+//   // 悬停
+//   --color-main-hover: var(--colors-primary-hover);
+//   // 点击
+//   --color-main-focus: var(--colors-primary-click);
+
+//   --editor-head-filter: blur(0px);
+//   --editor-head-height: 50px;
+
+//   --editor-head-back: rgba(20, 20, 20, 0.86);
+
+//   --editor-menu-filter: var(--editor-head-filter);
+//   --editor-menu-width: 80px;
+//   --editor-menu-left: 0px;
+//   --editor-menu-right: 0px;
+//   --editer-menu-fill: 27, 27, 28;
+//   --editor-menu-back: rgba(var(--editer-menu-fill), 0.8);
+//   --editor-menu-active-back: rgba(var(--colors-primary-fill), 0.06);
+//   --editor-menu-color: #999;
+//   --editor-menu-active: rgba(255, 255, 255, 0.06);
+
+//   --editor-toolbox-top: var(--editor-head-height);
+//   --editor-toolbox-left: calc(
+//     var(--editor-menu-left) + var(--editor-menu-width)
+//   );
+//   --editor-toolbox-width: 340px;
+//   --editor-toolbox-back: var(--editor-menu-back);
+//   --editor-toolbox-padding: 20px;
+//   --editor-toolbar-height: 60px;
+// }
+
 @use 'sass:map';
 
+// CSS3 var
+@use 'common/var' as *;
+@use 'mixins/var' as *;
+@use 'mixins/mixins' as *;
+
+// for better performance do not dynamically change the root variable if you really
+// do not need that, since this could introduce recalculation overhead for rendering.
+// https://lisilinhart.info/posts/css-variables-performance/
+
+// common
+:root {
+  @include set-css-var-value('color-white', $color-white);
+  @include set-css-var-value('color-black', $color-black);
+
+  // get rgb
+  @each $type in (primary, success, warning, danger, error, info) {
+    @include set-css-color-rgb($type);
+  }
+
+  // Typography
+  @include set-component-css-var('font-size', $font-size);
+  @include set-component-css-var('font-family', $font-family);
+
+  @include set-css-var-value('font-weight-primary', 500);
+  @include set-css-var-value('font-line-height-primary', 24px);
+
+  // z-index --el-index-#{$type}
+  @include set-component-css-var('index', $z-index);
+
+  // --el-border-radius-#{$type}
+  @include set-component-css-var('border-radius', $border-radius);
+
+  // Transition
+  // refer to this website to get the bezier motion function detail
+  // https://cubic-bezier.com/#p1,p2,p3,p4 (change px as your function parameter)
+  @include set-component-css-var('transition-duration', $transition-duration);
+
+  @include set-component-css-var('transition-function', $transition-function);
+  @include set-component-css-var('transition', $transition);
+
+  // common component size
+  @include set-component-css-var('component-size', $common-component-size);
+}
+
+// for light
 :root {
-    --colors-primary-fill: 255, 255, 255;
-    --colors-primary-base-fill: 0, 200, 175;
-    --colors-primary-base: rgb(var(--colors-primary-base-fill));
-    --colors-primary-hover: #4dd8c7;
-    // --colors-primary-hover: #008B7A;
-    --colors-primary-active: #008b7a;
-    --colors-primary-click: #005046;
-    --colors-warn: #fa3f48;
-    --colors-color: #999;
-    --colors-border-color: rgba(var(--colors-primary-fill), 0.16);
-    --colors-content-color: rgb(--colors-primary-fill);
-
-    --colors-normal-back: rgba(var(--colors-primary-fill), 0.1);
-    --colors-normal-base: rgba(var(--colors-primary-fill), 0.4);
-    --colors-normal-hover: rgba(var(--colors-primary-fill), 1);
-    --colors-normal-click: var(--colors-primary-click);
-
-    --colors-normal-fill-back: var(--colors-normal-back);
-    --colors-normal-fill-base: var(--colors-normal-base);
-    --colors-normal-fill-hover: var(--colors-normal-hover);
-    --colors-normal-fill-click: var(--colors-primary-click);
-
-    --colors-error-fill: 250, 63, 72;
-
-    --small-size: 12px;
-    --medium-size: 14px;
-    --big-size: 16px;
-
-    // 正常
-    --color-main-normal: var(--colors-primary-base);
-    // 悬停
-    --color-main-hover: var(--colors-primary-hover);
-    // 点击
-    --color-main-focus: var(--colors-primary-click);
-
-    --editor-head-filter: blur(0px);
-    --editor-head-height: 50px;
-
-    --editor-head-back: rgba(20, 20, 20, 0.86);
-
-    --editor-menu-filter: var(--editor-head-filter);
-    --editor-menu-width: 80px;
-    --editor-menu-left: 0px;
-    --editor-menu-right: 0px;
-    --editer-menu-fill: 27, 27, 28;
-    --editor-menu-back: rgba(var(--editer-menu-fill), 0.8);
-    --editor-menu-active-back: rgba(var(--colors-primary-fill), 0.06);
-    --editor-menu-color: #999;
-    --editor-menu-active: rgba(255, 255, 255, 0.06);
-
-    --editor-toolbox-top: var(--editor-head-height);
-    --editor-toolbox-left: calc(var(--editor-menu-left) + var(--editor-menu-width));
-    --editor-toolbox-width: 340px;
-    --editor-toolbox-back: var(--editor-menu-back);
-    --editor-toolbox-padding: 20px;
-    --editor-toolbar-height: 60px;
+  color-scheme: light;
+
+  @include set-css-var-value('color-white', $color-white);
+  @include set-css-var-value('color-black', $color-black);
+
+  // --el-color-#{$type}
+  // --el-color-#{$type}-light-{$i}
+  @each $type in (primary, success, warning, danger, error, info) {
+    @include set-css-color-type($colors, $type);
+  }
+
+  // color-scheme
+  // Background --el-bg-color-#{$type}
+  @include set-component-css-var('bg-color', $bg-color);
+  // --el-text-color-#{$type}
+  @include set-component-css-var('text-color', $text-color);
+  // --el-border-color-#{$type}
+  @include set-component-css-var('border-color', $border-color);
+  // Fill --el-fill-color-#{$type}
+  @include set-component-css-var('fill-color', $fill-color);
+
+  // Box-shadow
+  // --el-box-shadow-#{$type}
+  @include set-component-css-var('box-shadow', $box-shadow);
+  // Disable base
+  @include set-component-css-var('disabled', $disabled);
+
+  // overlay & mask
+  @include set-component-css-var('overlay-color', $overlay-color);
+  @include set-component-css-var('mask-color', $mask-color);
+
+  // Border
+  @include set-css-var-value('border-width', $border-width);
+  @include set-css-var-value('border-style', $border-style);
+  @include set-css-var-value('border-color-hover', $border-color-hover);
+  @include set-css-var-value(
+    'border',
+    getCssVar('border-width') getCssVar('border-style')
+      getCssVar('border-color')
+  );
+
+  // Svg
+  @include css-var-from-global('svg-monochrome-grey', 'border-color');
 }

+ 12 - 0
packages/tokens/button.ts

@@ -0,0 +1,12 @@
+import type { InjectionKey } from 'vue'
+
+import type { ButtonProps } from '@kankan-components/components/basic/button'
+
+export interface ButtonGroupContext {
+  size?: ButtonProps['size']
+  type?: ButtonProps['type']
+}
+
+export const buttonGroupContextKey: InjectionKey<ButtonGroupContext> = Symbol(
+  'buttonGroupContextKey'
+)

+ 14 - 0
packages/tokens/dialog.ts

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

+ 2 - 0
packages/tokens/index.ts

@@ -1 +1,3 @@
 export * from './config-provider'
+export * from './dialog'
+export * from './button'

+ 1 - 0
packages/utils/vue/icon.ts

@@ -8,6 +8,7 @@ import {
   SuccessFilled,
   WarningFilled,
 } from '@element-plus/icons-vue'
+
 import { definePropType } from './props'
 
 import type { Component } from 'vue'

+ 1 - 0
packages/utils/vue/index.ts

@@ -1,3 +1,4 @@
+export * from './icon'
 export * from './global-node'
 export * from './install'
 export * from './props'

+ 347 - 322
playground/src/components/advance/tag/src/metas/metasImage.vue

@@ -1,382 +1,407 @@
 <!--  -->
 <template>
-    <div class="pic-box" :class="{ show: viewer }" :style="metasHeight ? `height:${metasHeight}px;` : ''">
-        <div>
-            <div v-if="currentIndex != 0" class="ctrl-btn left-btn" @click.stop="changeImage('pre')">
-                <ui-icon type="left" />
-            </div>
-            <div v-if="currentIndex < data.length - 1" class="ctrl-btn right-btn" @click.stop="changeImage('next')">
-                <ui-icon type="right" />
-            </div>
-        </div>
-        <div class="over-box">
-            <div class="image-list" :style="`transform:translateX(${-100 * currentIndex}%);`">
-                <div v-for="(i, index) in data" :key="index" class="image-item" :style="`transform:translateX(${100 * index}%);background-image:url(${changeUrl(i.src)});`" />
-            </div>
-            <ui-icon class="loading-icon" type="_loading_" />
-            <!-- <div class="del-btn">
-                <ui-icon type="del"></ui-icon>
+  <div
+    class="pic-box"
+    :class="{ show: viewer }"
+    :style="metasHeight ? `height:${metasHeight}px;` : ''"
+  >
+    <div>
+      <div
+        v-if="currentIndex != 0"
+        class="ctrl-btn left-btn"
+        @click.stop="changeImage('pre')"
+      >
+        <kk-icon type="left" />
+      </div>
+      <div
+        v-if="currentIndex < data.length - 1"
+        class="ctrl-btn right-btn"
+        @click.stop="changeImage('next')"
+      >
+        <kk-icon type="right" />
+      </div>
+    </div>
+    <div class="over-box">
+      <div
+        class="image-list"
+        :style="`transform:translateX(${-100 * currentIndex}%);`"
+      >
+        <div
+          v-for="(i, index) in data"
+          :key="index"
+          class="image-item"
+          :style="`transform:translateX(${
+            100 * index
+          }%);background-image:url(${changeUrl(i.src)});`"
+        />
+      </div>
+      <kk-icon class="loading-icon" type="_loading_" />
+      <!-- <div class="del-btn">
+                <kk-icon type="del"></kk-icon>
             </div> -->
-        </div>
-        <div class="continue">
-            <span class="pic-num">
-                <span class="cur">{{ currentIndex + 1 }}</span>
-                <span><span>&nbsp;</span>/<span>&nbsp;</span></span>
-                <span>{{ data.length }}</span>
-            </span>
-        </div>
-        <!-- 移动端缩放 -->
     </div>
+    <div class="continue">
+      <span class="pic-num">
+        <span class="cur">{{ currentIndex + 1 }}</span>
+        <span><span>&nbsp;</span>/<span>&nbsp;</span></span>
+        <span>{{ data.length }}</span>
+      </span>
+    </div>
+    <!-- 移动端缩放 -->
+  </div>
 </template>
 
 <script lang="ts">
 import { defineComponent, inject, ref } from 'vue'
 import { buildProps } from '@kankan-components/utils'
-import { UIIcon } from '@kankan-components/components'
+import { KkIcon } from '@kankan-components/components'
 import type { PropType } from 'vue'
 
 const currentIndex = ref(0)
 
 const props = buildProps({
-    metasHeight: {
-        type: Number,
-        default: null,
-    },
-    data: {
-        type: Array as PropType<SourceType[]>,
-        default: () => [],
-    },
-    viewer: {
-        type: Boolean,
-        default: false,
-    },
-    scale: {
-        type: Boolean,
-        default: false,
-    },
+  metasHeight: {
+    type: Number,
+    default: null,
+  },
+  data: {
+    type: Array as PropType<SourceType[]>,
+    default: () => [],
+  },
+  viewer: {
+    type: Boolean,
+    default: false,
+  },
+  scale: {
+    type: Boolean,
+    default: false,
+  },
 })
 
 export default defineComponent({
-    name: 'MetaImage',
-    components: {
-        'ui-icon': UIIcon,
-    },
-    props,
-    setup() {
-        const __sdk: any = inject('__sdk')
-        function changeUrl(name: string, now?: string) {
-            if (name.includes('http')) {
-                return name
-            } else {
-                if ((typeof name === 'string' && name.slice(0, 4) == 'blob') || (typeof name === 'string' && name.slice(0, 10) == 'data:image')) {
-                    return name
-                } else {
-                    return __sdk.resource.getUserResourceURL(name, false, now)
-                }
-            }
-        }
-        const changeImage = (type: 'pre' | 'next') => {
-            if (type == 'pre') {
-                currentIndex.value--
-            } else {
-                currentIndex.value++
-            }
+  name: 'MetaImage',
+  components: {
+    'kk-icon': KkIcon,
+  },
+  props,
+  setup() {
+    const __sdk: any = inject('__sdk')
+    function changeUrl(name: string, now?: string) {
+      if (name.includes('http')) {
+        return name
+      } else {
+        if (
+          (typeof name === 'string' && name.slice(0, 4) == 'blob') ||
+          (typeof name === 'string' && name.slice(0, 10) == 'data:image')
+        ) {
+          return name
+        } else {
+          return __sdk.resource.getUserResourceURL(name, false, now)
         }
+      }
+    }
+    const changeImage = (type: 'pre' | 'next') => {
+      if (type == 'pre') {
+        currentIndex.value--
+      } else {
+        currentIndex.value++
+      }
+    }
 
-        return {
-            changeUrl,
-            changeImage,
-            currentIndex,
-        }
-    },
+    return {
+      changeUrl,
+      changeImage,
+      currentIndex,
+    }
+  },
 })
 </script>
 <style lang="scss" scoped>
 .showPicBox {
+  width: 100%;
+  height: 100%;
+  position: fixed;
+  z-index: 10000;
+  background: rgb(24, 22, 22);
+  top: 0;
+  left: 0;
+  .close {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    width: 20px;
+    height: 20px;
+    z-index: 100;
+    color: #fff;
+    .iconfont {
+      font-size: 20px;
+    }
+  }
+  .loading {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+  }
+  .imgbox {
     width: 100%;
     height: 100%;
-    position: fixed;
-    z-index: 10000;
-    background: rgb(24, 22, 22);
-    top: 0;
-    left: 0;
-    .close {
-        position: absolute;
-        top: 10px;
-        right: 10px;
-        width: 20px;
-        height: 20px;
-        z-index: 100;
-        color: #fff;
-        .iconfont {
-            font-size: 20px;
-        }
-    }
-    .loading {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
-    }
-    .imgbox {
-        width: 100%;
-        height: 100%;
-        background-repeat: no-repeat;
-        background-size: contain;
-        background-position: center center;
-        #eleImg {
-            // position: absolute;
+    background-repeat: no-repeat;
+    background-size: contain;
+    background-position: center center;
+    #eleImg {
+      // position: absolute;
 
-            // top: 50%;
-            // left: 50%;
-            // transform: translate(-50%, -50%);
-            margin: 0 auto;
-            display: block;
-            &.s {
-                height: 100%;
-                width: auto;
-            }
-            &.h {
-                height: auto;
-                width: 100%;
-            }
-        }
+      // top: 50%;
+      // left: 50%;
+      // transform: translate(-50%, -50%);
+      margin: 0 auto;
+      display: block;
+      &.s {
+        height: 100%;
+        width: auto;
+      }
+      &.h {
+        height: auto;
+        width: 100%;
+      }
     }
+  }
 }
 .del-btn {
-    width: 24px;
-    height: 24px;
-    background: rgba(0, 0, 0, 0.6);
-    border-radius: 50%;
-    position: absolute;
-    cursor: pointer;
-    top: 10px;
-    right: 10px;
-    z-index: 10;
-    display: flex;
-    align-items: center;
-    justify-content: center;
+  width: 24px;
+  height: 24px;
+  background: rgba(0, 0, 0, 0.6);
+  border-radius: 50%;
+  position: absolute;
+  cursor: pointer;
+  top: 10px;
+  right: 10px;
+  z-index: 10;
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 .loading-icon {
-    color: var(--editor-main-color);
-    animation: rotate 2s infinite linear;
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    font-size: 30px;
+  color: var(--editor-main-color);
+  animation: rotate 2s infinite linear;
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  font-size: 30px;
 }
 @keyframes rotate {
-    0% {
-        transform: translate(-50%, -50%) rotate(0deg);
-    }
-    100% {
-        transform: translate(-50%, -50%) rotate(360deg);
-    }
+  0% {
+    transform: translate(-50%, -50%) rotate(0deg);
+  }
+  100% {
+    transform: translate(-50%, -50%) rotate(360deg);
+  }
 }
 .pic-box {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  border-radius: 4px;
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  top: 0;
+  left: 0;
+  z-index: 10;
+
+  .over-box {
     width: 100%;
     height: 100%;
+    overflow: hidden;
+  }
+  .continue {
+    width: 100%;
+    height: 32px;
+    background: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, #000000 200%);
+    border-radius: 0px 0px 4px 4px;
     position: absolute;
-    border-radius: 4px;
-    border: 1px solid rgba(255, 255, 255, 0.2);
-    top: 0;
+    bottom: 0;
     left: 0;
-    z-index: 10;
 
-    .over-box {
-        width: 100%;
-        height: 100%;
-        overflow: hidden;
+    .ui-input {
+      width: 100%;
     }
-    .continue {
-        width: 100%;
-        height: 32px;
-        background: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, #000000 200%);
-        border-radius: 0px 0px 4px 4px;
-        position: absolute;
-        bottom: 0;
-        left: 0;
+    .continue-tips {
+      font-size: 12px;
+      margin-right: 5px;
+    }
+    .edit-pic-num {
+      // position: absolute;
+      // right: 10px;
+      font-size: 12px;
+      .cur {
+        color: var(--editor-main-color);
+      }
+    }
+    .pic-num {
+      position: absolute;
+      right: 10px;
+      top: 50%;
+      transform: translateY(-50%);
+      font-size: 12px;
+      .cur {
+        color: var(--editor-main-color);
+      }
+    }
+  }
 
-        .ui-input {
-            width: 100%;
-        }
-        .continue-tips {
-            font-size: 12px;
-            margin-right: 5px;
-        }
-        .edit-pic-num {
-            // position: absolute;
-            // right: 10px;
-            font-size: 12px;
-            .cur {
-                color: var(--editor-main-color);
-            }
+  .ctrl-btn {
+    width: 32px;
+    height: 32px;
+    background: rgba(0, 0, 0, 0.2);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 50%;
+    transform: translateY(-50%);
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .iconfont {
+      font-size: 14px;
+    }
+    &.left-btn {
+      left: 5px;
+    }
+    &.right-btn {
+      right: 5px;
+    }
+  }
+  .image-list {
+    width: 100%;
+    height: 100%;
+    position: relative;
+    transition: all 0.3s linear;
+    .image-item {
+      width: 100%;
+      height: 100%;
+      // background: red;
+      position: absolute;
+      transform: translateX(0);
+      text-align: center;
+      background-repeat: no-repeat;
+      background-size: contain;
+      background-position: center;
+      img {
+        height: 100%;
+        width: auto;
+      }
+    }
+  }
+  &.show {
+    .ctrl-btn {
+      width: 40px;
+      height: 80px;
+      background: rgba(0, 0, 0, 0.6);
+      .iconfont {
+        font-size: 20px;
+      }
+      &.left-btn {
+        left: 0px;
+        border-radius: 0 40px 40px 0;
+        .icon {
+          margin-right: 5px;
         }
-        .pic-num {
-            position: absolute;
-            right: 10px;
-            top: 50%;
-            transform: translateY(-50%);
-            font-size: 12px;
-            .cur {
-                color: var(--editor-main-color);
-            }
+      }
+      &.right-btn {
+        right: 0px;
+        border-radius: 40px 0 0 40px;
+        .icon {
+          margin-left: 8px;
         }
+      }
     }
+    .continue {
+      width: 76px;
+      height: 36px;
+      background: rgba(0, 0, 0, 0.6);
+      border-radius: 20px;
+      position: absolute;
+      bottom: -5%;
+      left: 50%;
+      transform: translateX(-50%);
 
-    .ctrl-btn {
-        width: 32px;
-        height: 32px;
-        background: rgba(0, 0, 0, 0.2);
-        border-radius: 50%;
-        position: absolute;
-        cursor: pointer;
-        top: 50%;
-        transform: translateY(-50%);
-        z-index: 10;
+      .pic-num {
+        width: 76px;
+        height: 36px;
+        display: inline-block;
         display: flex;
         align-items: center;
         justify-content: center;
+        font-size: 20px;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        span {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+  }
+}
+
+[is-mobile] {
+  .pic-box {
+    &.show {
+      .ctrl-btn {
+        width: 40px;
+        height: 80px;
+        background: rgba(0, 0, 0, 0.6);
         .iconfont {
-            font-size: 14px;
+          font-size: 20px;
         }
         &.left-btn {
-            left: 5px;
+          left: 0px;
+          border-radius: 0 40px 40px 0;
+          .icon {
+            margin-right: 5px;
+          }
         }
         &.right-btn {
-            right: 5px;
-        }
-    }
-    .image-list {
-        width: 100%;
-        height: 100%;
-        position: relative;
-        transition: all 0.3s linear;
-        .image-item {
-            width: 100%;
-            height: 100%;
-            // background: red;
-            position: absolute;
-            transform: translateX(0);
-            text-align: center;
-            background-repeat: no-repeat;
-            background-size: contain;
-            background-position: center;
-            img {
-                height: 100%;
-                width: auto;
-            }
-        }
-    }
-    &.show {
-        .ctrl-btn {
-            width: 40px;
-            height: 80px;
-            background: rgba(0, 0, 0, 0.6);
-            .iconfont {
-                font-size: 20px;
-            }
-            &.left-btn {
-                left: 0px;
-                border-radius: 0 40px 40px 0;
-                .icon {
-                    margin-right: 5px;
-                }
-            }
-            &.right-btn {
-                right: 0px;
-                border-radius: 40px 0 0 40px;
-                .icon {
-                    margin-left: 8px;
-                }
-            }
+          right: 0px;
+          border-radius: 40px 0 0 40px;
+          .icon {
+            margin-left: 8px;
+          }
         }
-        .continue {
-            width: 76px;
-            height: 36px;
-            background: rgba(0, 0, 0, 0.6);
-            border-radius: 20px;
-            position: absolute;
-            bottom: -5%;
-            left: 50%;
-            transform: translateX(-50%);
-
-            .pic-num {
-                width: 76px;
-                height: 36px;
-                display: inline-block;
-                display: flex;
-                align-items: center;
-                justify-content: center;
-                font-size: 20px;
-                top: 50%;
-                left: 50%;
-                transform: translate(-50%, -50%);
-                span {
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                }
-            }
-        }
-    }
-}
-
-[is-mobile] {
-    .pic-box {
-        &.show {
-            .ctrl-btn {
-                width: 40px;
-                height: 80px;
-                background: rgba(0, 0, 0, 0.6);
-                .iconfont {
-                    font-size: 20px;
-                }
-                &.left-btn {
-                    left: 0px;
-                    border-radius: 0 40px 40px 0;
-                    .icon {
-                        margin-right: 5px;
-                    }
-                }
-                &.right-btn {
-                    right: 0px;
-                    border-radius: 40px 0 0 40px;
-                    .icon {
-                        margin-left: 8px;
-                    }
-                }
-            }
-            .continue {
-                width: 76px;
-                height: 36px;
-                background: rgba(0, 0, 0, 0.6);
-                border-radius: 20px;
-                position: absolute;
-                bottom: -6%;
-                left: 50%;
-                transform: translateX(-50%);
+      }
+      .continue {
+        width: 76px;
+        height: 36px;
+        background: rgba(0, 0, 0, 0.6);
+        border-radius: 20px;
+        position: absolute;
+        bottom: -6%;
+        left: 50%;
+        transform: translateX(-50%);
 
-                .pic-num {
-                    width: 76px;
-                    height: 36px;
-                    display: inline-block;
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                    font-size: 20px;
-                    top: 50%;
-                    left: 50%;
-                    transform: translate(-50%, -50%);
-                    span {
-                        display: flex;
-                        align-items: center;
-                        justify-content: center;
-                    }
-                }
-            }
+        .pic-num {
+          width: 76px;
+          height: 36px;
+          display: inline-block;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          font-size: 20px;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          span {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+          }
         }
+      }
     }
+  }
 }
 </style>

+ 2 - 2
playground/src/components/basic/button/index.ts

@@ -1,6 +1,6 @@
 import { withInstall } from '@kankan-components/utils'
 import Button from './src/button.vue'
 
-export const UIButton = withInstall(Button)
+export const UiButton = withInstall(Button)
 
-export default UIButton
+export default UiButton

+ 15 - 15
playground/src/components/basic/button/src/button.vue

@@ -1,19 +1,19 @@
 <template>
-    <button class="ui-button" :class="className" :style="style">
-        <UIIcon v-if="icon" :type="icon" class="ui-button-icon" />
-        <slot />
-    </button>
+  <button class="ui-button" :class="className" :style="style">
+    <kk-icon v-if="icon" :type="icon" class="ui-button-icon" />
+    <slot />
+  </button>
 </template>
 
 <script lang="ts" setup>
 import { computed, defineProps } from 'vue'
 import { normalizeUnitToStyle } from '@kankan-components/utils'
-// import UIIcon from '../../icon/src/icon.vue'
-import UIIcon from '@kankan-components/components/basic/icon'
+// import KkIcon from '../../icon/src/icon.vue'
+import KkIcon from '@kankan-components/components/basic/icon'
 import { buttonProps } from './button'
 
 defineOptions({
-    name: 'UiButton',
+  name: 'UiButton',
 })
 const props = defineProps(buttonProps)
 
@@ -21,14 +21,14 @@ const custom = `customize`
 const className = computed(() => (props.color ? custom : props.type))
 
 const style = computed(() => {
-    const style = {
-        width: props.width ? normalizeUnitToStyle(props.width) : 'auto',
-        '--color': '',
-    }
+  const style = {
+    width: props.width ? normalizeUnitToStyle(props.width) : 'auto',
+    '--color': '',
+  }
 
-    if (className.value === custom) {
-        style['--color'] = props.color || ''
-    }
-    return style
+  if (className.value === custom) {
+    style['--color'] = props.color || ''
+  }
+  return style
 })
 </script>

+ 59 - 48
playground/src/components/basic/guide/index.vue

@@ -1,31 +1,42 @@
 <template>
-    <div v-if="mount && (msg || $slots.default)" ref="guideRef" class="guide" :class="{ 'floating-mode': floatClass }">
-        <slot name="content" :show="shouldShow" />
-        <UIFloating v-if="floatClass" :mount="mountEl" :refer="guideRef" dire="bottom" :class="`guide-floating ${props.floatClass}  ${type}`">
-            <Bubble :show="shouldShow" class="guide-bubble" :type="type">
-                <template v-if="msg">
-                    <p class="default-msg">{{ msg }}</p>
-                </template>
-                <slot v-else />
+  <div
+    v-if="mount && (msg || $slots.default)"
+    ref="guideRef"
+    class="guide"
+    :class="{ 'floating-mode': floatClass }"
+  >
+    <slot name="content" :show="shouldShow" />
+    <UIFloating
+      v-if="floatClass"
+      :mount="mountEl"
+      :refer="guideRef"
+      dire="bottom"
+      :class="`guide-floating ${props.floatClass}  ${type}`"
+    >
+      <Bubble :show="shouldShow" class="guide-bubble" :type="type">
+        <template v-if="msg">
+          <p class="default-msg">{{ msg }}</p>
+        </template>
+        <slot v-else />
 
-                <span class="guide-close" @click="shouldShow = false">
-                    <ui-icon type="close" ctrl />
-                </span>
-            </Bubble>
-        </UIFloating>
+        <span class="guide-close" @click="shouldShow = false">
+          <kk-icon type="close" ctrl />
+        </span>
+      </Bubble>
+    </UIFloating>
 
-        <Bubble v-else :show="shouldShow" class="guide-bubble" :type="type">
-            <template v-if="msg">
-                <p class="default-msg">{{ msg }}</p>
-            </template>
-            <slot v-else />
+    <Bubble v-else :show="shouldShow" class="guide-bubble" :type="type">
+      <template v-if="msg">
+        <p class="default-msg">{{ msg }}</p>
+      </template>
+      <slot v-else />
 
-            <span class="guide-close" @click="shouldShow = false">
-                <ui-icon type="close" ctrl />
-            </span>
-        </Bubble>
-    </div>
-    <slot v-else name="content" :show="false" />
+      <span class="guide-close" @click="shouldShow = false">
+        <kk-icon type="close" ctrl />
+      </span>
+    </Bubble>
+  </div>
+  <slot v-else name="content" :show="false" />
 </template>
 
 <script lang="ts" setup>
@@ -34,41 +45,41 @@ import UIFloating from '@kankan-components/components/basic/floating'
 import Bubble from '../bubble'
 
 defineOptions({
-    name: 'UIGuide',
+  name: 'UIGuide',
 })
 
 const props = defineProps({
-    mark: {
-        type: String,
-    },
-    msg: {
-        type: String,
-    },
-    floatClass: {
-        type: String,
-    },
-    type: {
-        type: String,
-        default: 'top',
-    },
+  mark: {
+    type: String,
+  },
+  msg: {
+    type: String,
+  },
+  floatClass: {
+    type: String,
+  },
+  type: {
+    type: String,
+    default: 'top',
+  },
 })
 
 const mountEl = document.body
 const guideRef = ref()
 const shouldShow = ref(true)
 if (props.mark) {
-    shouldShow.value = !localStorage.getItem(props.mark)
-    if (shouldShow.value) {
-        watch(shouldShow, (newv, oldv) => {
-            if (!newv && oldv) {
-                setTimeout(() => {
-                    localStorage.setItem(props.mark, 1)
-                })
-            }
+  shouldShow.value = !localStorage.getItem(props.mark)
+  if (shouldShow.value) {
+    watch(shouldShow, (newv, oldv) => {
+      if (!newv && oldv) {
+        setTimeout(() => {
+          localStorage.setItem(props.mark, 1)
         })
-    }
+      }
+    })
+  }
 } else {
-    shouldShow.value = true
+  shouldShow.value = true
 }
 
 const mount = ref(shouldShow.value)

+ 2 - 2
playground/src/components/basic/icon/index.ts

@@ -2,8 +2,8 @@ import { withInstall } from '@kankan-components/utils'
 
 import Icon from './src/icon.vue'
 
-export const UIIcon = withInstall(Icon)
+export const KkIcon = withInstall(Icon)
 
-export default UIIcon
+export default KkIcon
 
 export * from './src/icon'

+ 28 - 23
playground/src/components/basic/icon/src/icon.vue

@@ -1,9 +1,14 @@
 <template>
-    <i class="iconfont ui-kankan-icon icon" :class="className" :style="style" @click="ev => emit('click', ev)">
-        <slot />
+  <i
+    class="iconfont ui-kankan-icon icon"
+    :class="className"
+    :style="style"
+    @click="(ev) => emit('click', ev)"
+  >
+    <slot />
 
-        <p v-if="tip && os.isPc && !os.isTablet" class="tip">{{ tip }}</p>
-    </i>
+    <p v-if="tip && os.isPc && !os.isTablet" class="tip">{{ tip }}</p>
+  </i>
 </template>
 
 <script lang="ts" setup>
@@ -12,33 +17,33 @@ import { normalizeUnitToStyle, os } from '@kankan-components/utils'
 import { iconProps } from './icon'
 
 defineOptions({
-    name: 'UIIcon',
+  name: 'KkIcon',
 })
 
 const props = defineProps(iconProps)
 
 const style = computed(() => ({
-    'font-size': normalizeUnitToStyle(props.size || 14),
-    color: props.color,
+  'font-size': normalizeUnitToStyle(props.size || 14),
+  color: props.color,
 }))
 const className = computed(() => {
-    const base = {
-        small: props.small,
-        medium: props.medium,
-        big: props.big,
-        disabled: props.disabled,
-        [`tip-h-${props.tipH}`]: true,
-        [`tip-v-${props.tipV}`]: true,
-        ['fun-ctrl']: props.ctrl,
-    }
-    if (props.type) {
-        return {
-            ...base,
-            [`icon-${props.type}`]: props.type,
-        }
-    } else {
-        return base
+  const base = {
+    small: props.small,
+    medium: props.medium,
+    big: props.big,
+    disabled: props.disabled,
+    [`tip-h-${props.tipH}`]: true,
+    [`tip-v-${props.tipV}`]: true,
+    ['fun-ctrl']: props.ctrl,
+  }
+  if (props.type) {
+    return {
+      ...base,
+      [`icon-${props.type}`]: props.type,
     }
+  } else {
+    return base
+  }
 })
 
 const emit = defineEmits(['click'])

+ 21 - 18
playground/src/components/basic/input/src/checkbox/checkbox.vue

@@ -1,32 +1,35 @@
 <template>
-    <div class="input checkbox" :style="{ width, height }" :class="{ disabled }">
-        <input
-            :id="id"
-            :disabled="disabled"
-            type="checkbox"
-            class="replace-input"
-            :checked="props.modelValue"
-            @input="(ev:Event) => emit('update:modelValue', (ev.target as HTMLInputElement).checked)"
-        />
-        <span class="replace">
-            <UIIcon :type="props.modelValue ? 'checkbox' : 'nor'" :size="props.width > props.height ? props.height : props.width" />
-        </span>
-    </div>
-    <label v-if="props.label" class="label" :for="id">
-        {{ props.label }}
-    </label>
+  <div class="input checkbox" :style="{ width, height }" :class="{ disabled }">
+    <input
+      :id="id"
+      :disabled="disabled"
+      type="checkbox"
+      class="replace-input"
+      :checked="props.modelValue"
+      @input="(ev:Event) => emit('update:modelValue', (ev.target as HTMLInputElement).checked)"
+    />
+    <span class="replace">
+      <KkIcon
+        :type="props.modelValue ? 'checkbox' : 'nor'"
+        :size="props.width > props.height ? props.height : props.width"
+      />
+    </span>
+  </div>
+  <label v-if="props.label" class="label" :for="id">
+    {{ props.label }}
+  </label>
 </template>
 
 <script lang="ts" setup>
 import { defineEmits, defineProps } from 'vue'
 import { randomId } from '@kankan-components/utils'
-import UIIcon from '../../../icon/src/icon.vue'
+import KkIcon from '../../../icon/src/icon.vue'
 import { checkboxInputProps } from './checkbox'
 const props = defineProps(checkboxInputProps)
 const emit = defineEmits(['update:modelValue'])
 const id = randomId(4)
 
 defineOptions({
-    name: 'UICheckbox',
+  name: 'UICheckbox',
 })
 </script>

+ 0 - 0
playground/src/components/basic/input/src/file/file.vue


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels