Forráskód Böngészése

perf: replace vue-sonner with the toast component of shadcn-ui

vben 10 hónapja
szülő
commit
0eda99ec3b
23 módosított fájl, 536 hozzáadás és 131 törlés
  1. 2 1
      packages/@core/locales/src/langs/en-US.json
  2. 3 2
      packages/@core/locales/src/langs/zh-CN.json
  3. 2 2
      packages/@core/shared/iconify/src/create-icon.ts
  4. 45 38
      packages/@core/shared/iconify/src/material.ts
  5. 21 21
      packages/@core/shared/iconify/src/mdi.ts
  6. 1 2
      packages/@core/ui-kit/shadcn-ui/package.json
  7. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/index.ts
  8. 0 41
      packages/@core/ui-kit/shadcn-ui/src/components/ui/sonner/Sonner.vue
  9. 0 2
      packages/@core/ui-kit/shadcn-ui/src/components/ui/sonner/index.ts
  10. 35 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/Toast.vue
  11. 31 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastAction.vue
  12. 34 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastClose.vue
  13. 26 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastDescription.vue
  14. 11 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastProvider.vue
  15. 26 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastTitle.vue
  16. 29 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastViewport.vue
  17. 38 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/Toaster.vue
  18. 39 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/index.ts
  19. 168 0
      packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/use-toast.ts
  20. 1 0
      packages/business/universal-ui/src/index.ts
  21. 14 4
      packages/business/widgets/src/preferences/preferences.vue
  22. 9 9
      packages/icons/src/svg/index.ts
  23. 0 8
      pnpm-lock.yaml

+ 2 - 1
packages/@core/locales/src/langs/en-US.json

@@ -113,6 +113,8 @@
     "title": "Preferences",
     "subtitle": "Customize Preferences & Preview in Real Time",
     "reset-tip": "The data has changed, click to reset",
+    "reset-title": "Preferences reset",
+    "reset-success": "Preferences reset successfully",
     "appearance": "Appearance",
     "layout": "Layout",
     "content": "Content",
@@ -140,7 +142,6 @@
     "copy": "Copy Preferences",
     "copy-success": "Copy successful. Please replace in `src/preferences.ts` of the app",
     "clear-and-logout": "Clear Cache & Logout",
-    "reset-success": "Preferences reset successfully",
     "mode": "Mode",
     "logo-visible": "Display Logo",
     "general": "General",

+ 3 - 2
packages/@core/locales/src/langs/zh-CN.json

@@ -113,7 +113,9 @@
   "preferences": {
     "title": "偏好设置",
     "subtitle": "自定义偏好设置 & 实时预览",
+    "reset-title": "重置偏好设置",
     "reset-tip": "数据有变化,点击可进行重置",
+    "reset-success": "重置偏好设置成功",
     "appearance": "外观",
     "layout": "布局",
     "content": "内容",
@@ -137,9 +139,8 @@
     "plain": "朴素",
     "rounded": "圆润",
     "copy": "复制偏好设置",
-    "copy-success": "拷贝成功,请在 app 下的 `src/preferences.ts`内进行覆盖",
+    "copy-success": "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖",
     "clear-and-logout": "清空缓存 & 退出登录",
-    "reset-success": "重置偏好设置成功",
     "mode": "模式",
     "logo-visible": "显示 Logo",
     "general": "通用",

+ 2 - 2
packages/@core/shared/iconify/src/create-icon.ts

@@ -2,8 +2,8 @@ import { h } from 'vue';
 
 import { Icon } from '@iconify/vue';
 
-function createIcon(icon: string) {
+function createIconifyIcon(icon: string) {
   return h(Icon, { icon });
 }
 
-export { createIcon };
+export { createIconifyIcon };

+ 45 - 38
packages/@core/shared/iconify/src/material.ts

@@ -1,77 +1,84 @@
-import { createIcon } from './create-icon';
+import { createIconifyIcon } from './create-icon';
 
-export const IconDefault = createIcon('ic:round-auto-awesome');
+export const IconDefault = createIconifyIcon('ic:round-auto-awesome');
 
-export const IcRoundKeyboardArrowDown = createIcon(
+export const IcRoundKeyboardArrowDown = createIconifyIcon(
   'ic:round-keyboard-arrow-down',
 );
 
-export const IcRoundChevronRight = createIcon('ic:round-chevron-right');
+export const IcRoundChevronRight = createIconifyIcon('ic:round-chevron-right');
 
-export const IcRoundKeyboard = createIcon('ic:round-keyboard');
-// export const IcRoundMenuOpen = createIcon('ic:round-menu-open');
+export const IcRoundMenu = createIconifyIcon('ic:round-menu');
 
-export const IcRoundMenu = createIcon('ic:round-menu');
+export const IcRoundMoreHoriz = createIconifyIcon('ic:round-more-horiz');
 
-export const IcRoundMoreHoriz = createIcon('ic:round-more-horiz');
+export const IcRoundFitScreen = createIconifyIcon('ic:round-fit-screen');
 
-export const IcRoundFitScreen = createIcon('ic:round-fit-screen');
+export const IcTwotoneFitScreen = createIconifyIcon('ic:twotone-fit-screen');
 
-export const IcTwotoneFitScreen = createIcon('ic:twotone-fit-screen');
+export const IcRoundColorLens = createIconifyIcon('ic:round-color-lens');
 
-export const IcRoundColorLens = createIcon('ic:round-color-lens');
+export const IcRoundMoreVert = createIconifyIcon('ic:round-more-vert');
 
-export const IcRoundMoreVert = createIcon('ic:round-more-vert');
+export const IcRoundFullscreen = createIconifyIcon('ic:round-fullscreen');
 
-export const IcRoundFullscreen = createIcon('ic:round-fullscreen');
-
-export const IcRoundFullscreenExit = createIcon('ic:round-fullscreen-exit');
-
-export const IcRoundAutoAwesome = createIcon('ic:round-auto-awesome');
+export const IcRoundFullscreenExit = createIconifyIcon(
+  'ic:round-fullscreen-exit',
+);
 
-export const IcRoundClose = createIcon('ic:round-close');
+export const IcRoundClose = createIconifyIcon('ic:round-close');
 
-export const IcRoundRestartAlt = createIcon('ic:round-restart-alt');
+export const IcRoundRestartAlt = createIconifyIcon('ic:round-restart-alt');
 
-export const IcRoundLogout = createIcon('ic:round-logout');
+export const IcRoundLogout = createIconifyIcon('ic:round-logout');
 
-export const IcOutlineVisibility = createIcon('ic:outline-visibility');
+export const IcOutlineVisibility = createIconifyIcon('ic:outline-visibility');
 
-export const IcOutlineVisibilityOff = createIcon('ic:outline-visibility-off');
+export const IcOutlineVisibilityOff = createIconifyIcon(
+  'ic:outline-visibility-off',
+);
 
-export const IcRoundSearch = createIcon('ic:round-search');
+export const IcRoundSearch = createIconifyIcon('ic:round-search');
 
-export const IcRoundFolderCopy = createIcon('ic:round-folder-copy');
+export const IcRoundFolderCopy = createIconifyIcon('ic:round-folder-copy');
 
-export const IcRoundSubdirectoryArrowLeft = createIcon(
+export const IcRoundSubdirectoryArrowLeft = createIconifyIcon(
   'ic:round-subdirectory-arrow-left',
 );
-export const IcRoundArrowUpward = createIcon('ic:round-arrow-upward');
+export const IcRoundArrowUpward = createIconifyIcon('ic:round-arrow-upward');
 
-export const IcRoundArrowDownward = createIcon('ic:round-arrow-downward');
+export const IcRoundArrowDownward = createIconifyIcon(
+  'ic:round-arrow-downward',
+);
 
-export const IcBaselineLanguage = createIcon('ic:baseline-language');
+export const IcBaselineLanguage = createIconifyIcon('ic:baseline-language');
 
-export const IcRoundSearchOff = createIcon('ic:round-search-off');
+export const IcRoundSearchOff = createIconifyIcon('ic:round-search-off');
 
-export const IcRoundNotificationsNone = createIcon(
+export const IcRoundNotificationsNone = createIconifyIcon(
   'ic:round-notifications-none',
 );
 
-export const IcRoundMarkEmailRead = createIcon('ic:round-mark-email-read');
+export const IcRoundMarkEmailRead = createIconifyIcon(
+  'ic:round-mark-email-read',
+);
 
-export const IcRoundWbSunny = createIcon('ic:round-wb-sunny');
+export const IcRoundWbSunny = createIconifyIcon('ic:round-wb-sunny');
 
-export const IcRoundMotionPhotosAuto = createIcon(
+export const IcRoundMotionPhotosAuto = createIconifyIcon(
   'ic:round-motion-photos-auto',
 );
 
-export const IcRoundSettingsSuggest = createIcon('ic:round-settings-suggest');
+export const IcRoundSettingsSuggest = createIconifyIcon(
+  'ic:round-settings-suggest',
+);
 
-export const IcRoundArrowBackIosNew = createIcon('ic:round-arrow-back-ios-new');
+export const IcRoundArrowBackIosNew = createIconifyIcon(
+  'ic:round-arrow-back-ios-new',
+);
 
-export const IcRoundMultipleStop = createIcon('ic:round-multiple-stop');
+export const IcRoundMultipleStop = createIconifyIcon('ic:round-multiple-stop');
 
-export const IcRoundRefresh = createIcon('ic:round-refresh');
+export const IcRoundRefresh = createIconifyIcon('ic:round-refresh');
 
-export const IcRoundCreditScore = createIcon('ic:round-credit-score');
+export const IcRoundCreditScore = createIconifyIcon('ic:round-credit-score');

+ 21 - 21
packages/@core/shared/iconify/src/mdi.ts

@@ -1,49 +1,49 @@
-import { createIcon } from './create-icon';
+import { createIconifyIcon } from './create-icon';
 
-export const MdiKeyboardEsc = createIcon('mdi:keyboard-esc');
+export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc');
 
-export const MdiLoading = createIcon('mdi:loading');
+export const MdiLoading = createIconifyIcon('mdi:loading');
 
-export const MdiWechat = createIcon('mdi:wechat');
+export const MdiWechat = createIconifyIcon('mdi:wechat');
 
-export const MdiGithub = createIcon('mdi:github');
+export const MdiGithub = createIconifyIcon('mdi:github');
 
-export const MdiGoogle = createIcon('mdi:google');
+export const MdiGoogle = createIconifyIcon('mdi:google');
 
-export const MdiQqchat = createIcon('mdi:qqchat');
+export const MdiQqchat = createIconifyIcon('mdi:qqchat');
 
-export const MdiPin = createIcon('mdi:pin');
+export const MdiPin = createIconifyIcon('mdi:pin');
 
-export const MdiPinOff = createIcon('mdi:pin-off');
+export const MdiPinOff = createIconifyIcon('mdi:pin-off');
 
-export const MdiFormatHorizontalAlignLeft = createIcon(
+export const MdiFormatHorizontalAlignLeft = createIconifyIcon(
   'mdi:format-horizontal-align-left',
 );
 
-export const MdiFormatHorizontalAlignRight = createIcon(
+export const MdiFormatHorizontalAlignRight = createIconifyIcon(
   'mdi:format-horizontal-align-right',
 );
 
-export const MdiArrowExpandHorizontal = createIcon(
+export const MdiArrowExpandHorizontal = createIconifyIcon(
   'mdi:arrow-expand-horizontal',
 );
 
-export const MdiMenuClose = createIcon('mdi:menu-close');
+export const MdiMenuClose = createIconifyIcon('mdi:menu-close');
 
-export const MdiMenuOpen = createIcon('mdi:menu-open');
+export const MdiMenuOpen = createIconifyIcon('mdi:menu-open');
 
-export const MdiDockLeft = createIcon('mdi:dock-left');
+export const MdiDockLeft = createIconifyIcon('mdi:dock-left');
 
-export const MdiDockRight = createIcon('mdi:dock-right');
+export const MdiDockRight = createIconifyIcon('mdi:dock-right');
 
-export const MdiDockBottom = createIcon('mdi:dock-bottom');
+export const MdiDockBottom = createIconifyIcon('mdi:dock-bottom');
 
-export const MdiDriveDocument = createIcon('mdi:drive-document');
+export const MdiDriveDocument = createIconifyIcon('mdi:drive-document');
 
-export const MdiMoonAndStars = createIcon('mdi:moon-and-stars');
+export const MdiMoonAndStars = createIconifyIcon('mdi:moon-and-stars');
 
-export const MdiEditBoxOutline = createIcon('mdi:edit-box-outline');
+export const MdiEditBoxOutline = createIconifyIcon('mdi:edit-box-outline');
 
-export const MdiQuestionMarkCircleOutline = createIcon(
+export const MdiQuestionMarkCircleOutline = createIconifyIcon(
   'mdi:question-mark-circle-outline',
 );

+ 1 - 2
packages/@core/ui-kit/shadcn-ui/package.json

@@ -51,7 +51,6 @@
     "class-variance-authority": "^0.7.0",
     "lucide-vue-next": "^0.400.0",
     "radix-vue": "^1.9.0",
-    "vue": "^3.4.31",
-    "vue-sonner": "^1.1.3"
+    "vue": "^3.4.31"
   }
 }

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/components/index.ts

@@ -41,9 +41,9 @@ export * from './ui/popover';
 export * from './ui/scroll-area';
 export * from './ui/select';
 export * from './ui/sheet';
-export * from './ui/sonner';
 export * from './ui/switch';
 export * from './ui/tabs';
+export * from './ui/toast';
 export * from './ui/toggle';
 export * from './ui/toggle-group';
 export * from './ui/tooltip';

+ 0 - 41
packages/@core/ui-kit/shadcn-ui/src/components/ui/sonner/Sonner.vue

@@ -1,41 +0,0 @@
-<script lang="ts" setup>
-import { Toaster as Sonner, type ToasterProps } from 'vue-sonner';
-
-const props = withDefaults(defineProps<ToasterProps>(), {
-  closeButton: true,
-  duration: 2500,
-  position: 'top-right',
-  richColors: true,
-  visibleToasts: 3,
-});
-</script>
-
-<template>
-  <Sonner
-    class="toaster group"
-    v-bind="props"
-    :toast-options="{
-      classes: {
-        closeButton:
-          '!border-border group-[.toast]:bg-muted group-[.toast]:text-muted-foreground !bg-muted hover:!text-foreground',
-        toast:
-          'group toast group-[.toaster]:bg-background group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
-        description: 'group-[.toast]:text-muted-foreground',
-        actionButton:
-          'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
-        cancelButton:
-          'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
-      },
-    }"
-  />
-</template>
-
-<!-- <style scoped>
-:deep([data-sonner-toaster][data-theme='dark']),
-:deep([data-sonner-toaster][data-theme='light']) {
-  --normal-bg: hsl(var(--background));
-  --normal-border: theme('colors.border');
-  --normal-text: theme('colors.popover.foreground');
-  --border-radius: theme('borderRadius.md');
-}
-</style> -->

+ 0 - 2
packages/@core/ui-kit/shadcn-ui/src/components/ui/sonner/index.ts

@@ -1,2 +0,0 @@
-export { default as Toaster } from './Sonner.vue';
-export { toast } from 'vue-sonner';

+ 35 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/Toast.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import { computed } from 'vue';
+
+import { cn } from '@vben-core/toolkit';
+
+import {
+  ToastRoot,
+  type ToastRootEmits,
+  useForwardPropsEmits,
+} from 'radix-vue';
+
+import { type ToastProps, toastVariants } from '.';
+
+const props = defineProps<ToastProps>();
+
+const emits = defineEmits<ToastRootEmits>();
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props;
+
+  return delegated;
+});
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits);
+</script>
+
+<template>
+  <ToastRoot
+    v-bind="forwarded"
+    :class="cn(toastVariants({ variant }), props.class)"
+    @update:open="onOpenChange"
+  >
+    <slot></slot>
+  </ToastRoot>
+</template>

+ 31 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastAction.vue

@@ -0,0 +1,31 @@
+<script setup lang="ts">
+import { type HTMLAttributes, computed } from 'vue';
+
+import { cn } from '@vben-core/toolkit';
+
+import { ToastAction, type ToastActionProps } from 'radix-vue';
+
+const props = defineProps<
+  { class?: HTMLAttributes['class'] } & ToastActionProps
+>();
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props;
+
+  return delegated;
+});
+</script>
+
+<template>
+  <ToastAction
+    v-bind="delegatedProps"
+    :class="
+      cn(
+        'hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive border-border inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors focus:outline-none focus:ring-1 disabled:pointer-events-none disabled:opacity-50',
+        props.class,
+      )
+    "
+  >
+    <slot></slot>
+  </ToastAction>
+</template>

+ 34 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastClose.vue

@@ -0,0 +1,34 @@
+<script setup lang="ts">
+import { type HTMLAttributes, computed } from 'vue';
+
+import { cn } from '@vben-core/toolkit';
+
+import { Cross2Icon } from '@radix-icons/vue';
+import { ToastClose, type ToastCloseProps } from 'radix-vue';
+
+const props = defineProps<
+  {
+    class?: HTMLAttributes['class'];
+  } & ToastCloseProps
+>();
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props;
+
+  return delegated;
+});
+</script>
+
+<template>
+  <ToastClose
+    v-bind="delegatedProps"
+    :class="
+      cn(
+        'text-foreground/50 hover:text-foreground absolute right-1 top-1 rounded-md p-1 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
+        props.class,
+      )
+    "
+  >
+    <Cross2Icon class="h-4 w-4" />
+  </ToastClose>
+</template>

+ 26 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastDescription.vue

@@ -0,0 +1,26 @@
+<script setup lang="ts">
+import { type HTMLAttributes, computed } from 'vue';
+
+import { cn } from '@vben-core/toolkit';
+
+import { ToastDescription, type ToastDescriptionProps } from 'radix-vue';
+
+const props = defineProps<
+  { class?: HTMLAttributes['class'] } & ToastDescriptionProps
+>();
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props;
+
+  return delegated;
+});
+</script>
+
+<template>
+  <ToastDescription
+    :class="cn('text-sm opacity-90', props.class)"
+    v-bind="delegatedProps"
+  >
+    <slot></slot>
+  </ToastDescription>
+</template>

+ 11 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastProvider.vue

@@ -0,0 +1,11 @@
+<script setup lang="ts">
+import { ToastProvider, type ToastProviderProps } from 'radix-vue';
+
+const props = defineProps<ToastProviderProps>();
+</script>
+
+<template>
+  <ToastProvider v-bind="props">
+    <slot></slot>
+  </ToastProvider>
+</template>

+ 26 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastTitle.vue

@@ -0,0 +1,26 @@
+<script setup lang="ts">
+import { type HTMLAttributes, computed } from 'vue';
+
+import { cn } from '@vben-core/toolkit';
+
+import { ToastTitle, type ToastTitleProps } from 'radix-vue';
+
+const props = defineProps<
+  { class?: HTMLAttributes['class'] } & ToastTitleProps
+>();
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props;
+
+  return delegated;
+});
+</script>
+
+<template>
+  <ToastTitle
+    v-bind="delegatedProps"
+    :class="cn('text-sm font-semibold [&+div]:text-xs', props.class)"
+  >
+    <slot></slot>
+  </ToastTitle>
+</template>

+ 29 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/ToastViewport.vue

@@ -0,0 +1,29 @@
+<script setup lang="ts">
+import { type HTMLAttributes, computed } from 'vue';
+
+import { cn } from '@vben-core/toolkit';
+
+import { ToastViewport, type ToastViewportProps } from 'radix-vue';
+
+const props = defineProps<
+  { class?: HTMLAttributes['class'] } & ToastViewportProps
+>();
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props;
+
+  return delegated;
+});
+</script>
+
+<template>
+  <ToastViewport
+    v-bind="delegatedProps"
+    :class="
+      cn(
+        'fixed top-0 z-[1200] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
+        props.class,
+      )
+    "
+  />
+</template>

+ 38 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/Toaster.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+import { isVNode } from 'vue';
+
+import {
+  Toast,
+  ToastClose,
+  ToastDescription,
+  ToastProvider,
+  ToastTitle,
+  ToastViewport,
+} from '.';
+import { useToast } from './use-toast';
+
+const { toasts } = useToast();
+</script>
+
+<template>
+  <ToastProvider swipe-direction="down">
+    <Toast v-for="toast in toasts" :key="toast.id" v-bind="toast">
+      <div class="grid gap-1">
+        <ToastTitle v-if="toast.title">
+          {{ toast.title }}
+        </ToastTitle>
+        <template v-if="toast.description">
+          <ToastDescription v-if="isVNode(toast.description)">
+            <component :is="toast.description" />
+          </ToastDescription>
+          <ToastDescription v-else>
+            {{ toast.description }}
+          </ToastDescription>
+        </template>
+        <ToastClose />
+      </div>
+      <component :is="toast.action" />
+    </Toast>
+    <ToastViewport />
+  </ToastProvider>
+</template>

+ 39 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/index.ts

@@ -0,0 +1,39 @@
+import type { ToastRootProps } from 'radix-vue';
+
+import type { HTMLAttributes } from 'vue';
+
+import { type VariantProps, cva } from 'class-variance-authority';
+
+export { default as Toast } from './Toast.vue';
+export { default as ToastAction } from './ToastAction.vue';
+export { default as ToastClose } from './ToastClose.vue';
+export { default as ToastDescription } from './ToastDescription.vue';
+export { default as ToastProvider } from './ToastProvider.vue';
+export { default as ToastTitle } from './ToastTitle.vue';
+export { default as ToastViewport } from './ToastViewport.vue';
+export { default as Toaster } from './Toaster.vue';
+export { toast, useToast } from './use-toast';
+
+export const toastVariants = cva(
+  'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
+  {
+    defaultVariants: {
+      variant: 'default',
+    },
+    variants: {
+      variant: {
+        default: 'border bg-background border-border text-foreground',
+        destructive:
+          'destructive group border-destructive bg-destructive text-destructive-foreground',
+      },
+    },
+  },
+);
+
+type ToastVariants = VariantProps<typeof toastVariants>;
+
+export interface ToastProps extends ToastRootProps {
+  class?: HTMLAttributes['class'];
+  onOpenChange?: ((value: boolean) => void) | undefined;
+  variant?: ToastVariants['variant'];
+}

+ 168 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/toast/use-toast.ts

@@ -0,0 +1,168 @@
+import type { ToastProps } from '.';
+
+import { computed, ref } from 'vue';
+import type { Component, VNode } from 'vue';
+
+const TOAST_LIMIT = 1;
+const TOAST_REMOVE_DELAY = 1_000_000;
+
+export type StringOrVNode = (() => VNode) | VNode | string;
+
+type ToasterToast = {
+  action?: Component;
+  description?: StringOrVNode;
+  id: string;
+  title?: string;
+} & ToastProps;
+
+const actionTypes = {
+  ADD_TOAST: 'ADD_TOAST',
+  DISMISS_TOAST: 'DISMISS_TOAST',
+  REMOVE_TOAST: 'REMOVE_TOAST',
+  UPDATE_TOAST: 'UPDATE_TOAST',
+} as const;
+
+let count = 0;
+
+function genId() {
+  count = (count + 1) % Number.MAX_VALUE;
+  return count.toString();
+}
+
+type ActionType = typeof actionTypes;
+
+type Action =
+  | {
+      toast: Partial<ToasterToast>;
+      type: ActionType['UPDATE_TOAST'];
+    }
+  | {
+      toast: ToasterToast;
+      type: ActionType['ADD_TOAST'];
+    }
+  | {
+      toastId?: ToasterToast['id'];
+      type: ActionType['DISMISS_TOAST'];
+    }
+  | {
+      toastId?: ToasterToast['id'];
+      type: ActionType['REMOVE_TOAST'];
+    };
+
+interface State {
+  toasts: ToasterToast[];
+}
+
+const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
+
+function addToRemoveQueue(toastId: string) {
+  if (toastTimeouts.has(toastId)) return;
+
+  const timeout = setTimeout(() => {
+    toastTimeouts.delete(toastId);
+    dispatch({
+      toastId,
+      type: actionTypes.REMOVE_TOAST,
+    });
+  }, TOAST_REMOVE_DELAY);
+
+  toastTimeouts.set(toastId, timeout);
+}
+
+const state = ref<State>({
+  toasts: [],
+});
+
+function dispatch(action: Action) {
+  switch (action.type) {
+    case actionTypes.ADD_TOAST: {
+      state.value.toasts = [action.toast, ...state.value.toasts].slice(
+        0,
+        TOAST_LIMIT,
+      );
+      break;
+    }
+
+    case actionTypes.UPDATE_TOAST: {
+      state.value.toasts = state.value.toasts.map((t) =>
+        t.id === action.toast.id ? { ...t, ...action.toast } : t,
+      );
+      break;
+    }
+
+    case actionTypes.DISMISS_TOAST: {
+      const { toastId } = action;
+
+      if (toastId) {
+        addToRemoveQueue(toastId);
+      } else {
+        state.value.toasts.forEach((toast) => {
+          addToRemoveQueue(toast.id);
+        });
+      }
+
+      state.value.toasts = state.value.toasts.map((t) =>
+        t.id === toastId || toastId === undefined
+          ? {
+              ...t,
+              open: false,
+            }
+          : t,
+      );
+      break;
+    }
+
+    case actionTypes.REMOVE_TOAST: {
+      state.value.toasts =
+        action.toastId === undefined
+          ? []
+          : state.value.toasts.filter((t) => t.id !== action.toastId);
+
+      break;
+    }
+  }
+}
+
+function useToast() {
+  return {
+    dismiss: (toastId?: string) =>
+      dispatch({ toastId, type: actionTypes.DISMISS_TOAST }),
+    toast,
+    toasts: computed(() => state.value.toasts),
+  };
+}
+
+type Toast = Omit<ToasterToast, 'id'>;
+
+function toast(props: Toast) {
+  const id = genId();
+
+  const update = (props: ToasterToast) =>
+    dispatch({
+      toast: { ...props, id },
+      type: actionTypes.UPDATE_TOAST,
+    });
+
+  const dismiss = () =>
+    dispatch({ toastId: id, type: actionTypes.DISMISS_TOAST });
+
+  dispatch({
+    toast: {
+      ...props,
+      id,
+      onOpenChange: (open: boolean) => {
+        if (!open) dismiss();
+      },
+      open: true,
+    },
+    type: actionTypes.ADD_TOAST,
+  });
+
+  return {
+    dismiss,
+    id,
+    update,
+  };
+}
+
+export { toast, useToast };

+ 1 - 0
packages/business/universal-ui/src/index.ts

@@ -2,3 +2,4 @@ export * from './about';
 export * from './authentication';
 export * from './dashboard';
 export * from './fallback';
+export { useToast } from '@vben-core/shadcn-ui';

+ 14 - 4
packages/business/widgets/src/preferences/preferences.vue

@@ -28,7 +28,7 @@ import {
   VbenIconButton,
   VbenSegmented,
   VbenSheet,
-  toast,
+  useToast,
 } from '@vben-core/shadcn-ui';
 
 import { useClipboard } from '@vueuse/core';
@@ -56,7 +56,7 @@ import Trigger from './trigger.vue';
 import { useOpenPreferences } from './use-open-preferences';
 
 const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
-
+const { toast } = useToast();
 const appLocale = defineModel<SupportedLanguagesType>('appLocale');
 const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
 const appAiAssistant = defineModel<boolean>('appAiAssistant');
@@ -177,7 +177,10 @@ const { openPreferences } = useOpenPreferences();
 async function handleCopy() {
   await copy(JSON.stringify(diffPreference.value, null, 2));
 
-  toast($t('preferences.copy-success'));
+  toast({
+    description: $t('preferences.copy'),
+    title: $t('preferences.copy-success'),
+  });
 }
 
 async function handleClearCache() {
@@ -192,7 +195,14 @@ async function handleReset() {
   }
   resetPreferences();
   await loadLocaleMessages(preferences.app.locale);
-  toast($t('preferences.reset-success'));
+  toast({
+    description: $t('preferences.reset-title'),
+    title: $t('preferences.reset-success'),
+  });
+  toast({
+    description: $t('preferences.reset-title'),
+    title: $t('preferences.reset-success'),
+  });
 }
 </script>
 

+ 9 - 9
packages/icons/src/svg/index.ts

@@ -1,4 +1,4 @@
-import { createIcon } from '@vben-core/iconify';
+import { createIconifyIcon } from '@vben-core/iconify';
 
 import { loadSvgIcons } from './load';
 
@@ -8,14 +8,14 @@ if (!loaded) {
   loaded = true;
 }
 
-const SvgAvatar1Icon = createIcon('svg:avatar-1');
-const SvgAvatar2Icon = createIcon('svg:avatar-2');
-const SvgAvatar3Icon = createIcon('svg:avatar-3');
-const SvgAvatar4Icon = createIcon('svg:avatar-4');
-const SvgDownloadIcon = createIcon('svg:download');
-const SvgCardIcon = createIcon('svg:card');
-const SvgBellIcon = createIcon('svg:bell');
-const SvgCakeIcon = createIcon('svg:cake');
+const SvgAvatar1Icon = createIconifyIcon('svg:avatar-1');
+const SvgAvatar2Icon = createIconifyIcon('svg:avatar-2');
+const SvgAvatar3Icon = createIconifyIcon('svg:avatar-3');
+const SvgAvatar4Icon = createIconifyIcon('svg:avatar-4');
+const SvgDownloadIcon = createIconifyIcon('svg:download');
+const SvgCardIcon = createIconifyIcon('svg:card');
+const SvgBellIcon = createIconifyIcon('svg:bell');
+const SvgCakeIcon = createIconifyIcon('svg:cake');
 
 export {
   SvgAvatar1Icon,

+ 0 - 8
pnpm-lock.yaml

@@ -784,9 +784,6 @@ importers:
       vue:
         specifier: ^3.4.31
         version: 3.4.31(typescript@5.5.3)
-      vue-sonner:
-        specifier: ^1.1.3
-        version: 1.1.3
 
   packages/@core/ui-kit/tabs-ui:
     dependencies:
@@ -9183,9 +9180,6 @@ packages:
     peerDependencies:
       vue: ^3.4.31
 
-  vue-sonner@1.1.3:
-    resolution: {integrity: sha512-6I+5GNobKvE2nR5MPhO+T59d4j2LXRQoc/ZCmGtCoBWKDQr5nzSqjFaOOdPysHFI2p42wNLhQMafd0N540UW9Q==}
-
   vue-template-compiler@2.7.16:
     resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==}
 
@@ -18548,8 +18542,6 @@ snapshots:
       '@vue/devtools-api': 6.6.3
       vue: 3.4.31(typescript@5.5.3)
 
-  vue-sonner@1.1.3: {}
-
   vue-template-compiler@2.7.16:
     dependencies:
       de-indent: 1.0.2