chore: update uikit -> ui-kit

This commit is contained in:
vben
2024-06-23 20:03:41 +08:00
parent 89586ef2c4
commit d4f61c283f
351 changed files with 341 additions and 391 deletions

View File

@@ -0,0 +1,33 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: [
{
builder: 'mkdist',
input: './src',
pattern: ['**/*'],
},
{
builder: 'mkdist',
input: './src',
loaders: ['vue'],
pattern: ['**/*.vue'],
},
// {
// builder: 'mkdist',
// format: 'cjs',
// input: './src',
// loaders: ['js'],
// pattern: ['**/*.ts'],
// },
{
builder: 'mkdist',
format: 'esm',
input: './src',
loaders: ['js'],
pattern: ['**/*.ts'],
},
],
});

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "tailwind.config.mjs",
"css": "src/assets/index.css",
"baseColor": "slate",
"cssVariables": true
},
"framework": "vite",
"aliases": {
"components": "@vben-core/shadcn-ui/components",
"utils": "@vben-core/toolkit"
}
}

View File

@@ -0,0 +1,56 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.0.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/@vben-core/uikit/shadcn-ui"
},
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild",
"prepublishOnly": "npm run build"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.mjs"
},
"./*": {
"types": "./src/*/index.ts",
"development": "./src/*/index.ts",
"default": "./dist/*/index.mjs"
}
},
"publishConfig": {
"exports": {
".": {
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"@radix-icons/vue": "^1.0.0",
"@vben-core/colorful": "workspace:*",
"@vben-core/iconify": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",
"class-variance-authority": "^0.7.0",
"radix-vue": "^1.8.5",
"vue": "^3.4.30",
"vue-sonner": "^1.1.3"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config/postcss';

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialog as AlertDialogRoot,
AlertDialogTitle,
} from '@vben-core/shadcn-ui/components/ui/alert-dialog';
interface Props {
cancelText?: string;
content?: string;
submitText?: string;
title?: string;
}
withDefaults(defineProps<Props>(), {
cancelText: '取消',
submitText: '确认',
});
const emits = defineEmits<{
cancel: [];
submit: [];
}>();
const openModal = defineModel<boolean>('open');
function handleSubmit() {
emits('submit');
openModal.value = false;
}
function handleCancel() {
emits('cancel');
openModal.value = false;
}
</script>
<template>
<AlertDialogRoot v-model:open="openModal">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ title }}</AlertDialogTitle>
<AlertDialogDescription>
{{ content }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="handleCancel">
{{ cancelText }}
</AlertDialogCancel>
<AlertDialogAction @click="handleSubmit">
{{ submitText }}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogRoot>
</template>

View File

@@ -0,0 +1 @@
export { default as VbenAlertDialog } from './alert-dialog.vue';

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import type {
AvatarFallbackProps,
AvatarImageProps,
AvatarRootProps,
} from 'radix-vue';
import type { HTMLAttributes } from 'vue';
import { computed } from 'vue';
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@vben-core/shadcn-ui/components/ui/avatar';
interface Props extends AvatarRootProps, AvatarFallbackProps, AvatarImageProps {
alt?: string;
class?: HTMLAttributes['class'];
dot?: boolean;
dotClass?: HTMLAttributes['class'];
}
defineOptions({
inheritAttrs: false,
});
const props = withDefaults(defineProps<Props>(), {
alt: 'avatar',
as: 'button',
dot: false,
dotClass: 'bg-green-500',
});
const text = computed(() => {
return props.alt.slice(0, 2).toUpperCase();
});
</script>
<template>
<div :class="props.class" class="relative flex-shrink-0">
<Avatar :class="props.class" class="size-full">
<AvatarImage :alt="alt" :src="src" />
<AvatarFallback>{{ text }}</AvatarFallback>
</Avatar>
<span
v-if="dot"
:class="dotClass"
class="border-background absolute bottom-0 right-0 size-3 rounded-full border-2"
>
</span>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as VbenAvatar } from './avatar.vue';

View File

@@ -0,0 +1,43 @@
<script lang="ts" setup>
import type { BacktopProps } from './backtop';
import { computed } from 'vue';
import { IcRoundArrowUpward } from '@vben-core/iconify';
import { VbenButton } from '../button';
import { useBackTop } from './use-backtop';
interface Props extends BacktopProps {}
defineOptions({ name: 'BackTop' });
const props = withDefaults(defineProps<Props>(), {
bottom: 40,
isGroup: false,
right: 40,
target: '',
visibilityHeight: 200,
});
const backTopStyle = computed(() => ({
bottom: `${props.bottom}px`,
right: `${props.right}px`,
}));
const { handleClick, visible } = useBackTop(props);
</script>
<template>
<transition name="fade-down">
<VbenButton
v-if="visible"
:style="backTopStyle"
class="bg-accent data fixed bottom-10 right-5 h-10 w-10 rounded-full"
size="icon"
variant="icon"
@click="handleClick"
>
<IcRoundArrowUpward />
</VbenButton>
</transition>
</template>

View File

@@ -0,0 +1,38 @@
export const backtopProps = {
/**
* @zh_CN bottom distance.
*/
bottom: {
default: 40,
type: Number,
},
/**
* @zh_CN right distance.
*/
right: {
default: 40,
type: Number,
},
/**
* @zh_CN the target to trigger scroll.
*/
target: {
default: '',
type: String,
},
/**
* @zh_CN the button will not show until the scroll height reaches this value.
*/
visibilityHeight: {
default: 200,
type: Number,
},
} as const;
export interface BacktopProps {
bottom?: number;
isGroup?: boolean;
right?: number;
target?: string;
visibilityHeight?: number;
}

View File

@@ -0,0 +1 @@
export { default as VbenBackTop } from './back-top.vue';

View File

@@ -0,0 +1,45 @@
import type { BacktopProps } from './backtop';
import { onMounted, ref, shallowRef } from 'vue';
import { useEventListener, useThrottleFn } from '@vueuse/core';
export const useBackTop = (props: BacktopProps) => {
const el = shallowRef<HTMLElement>();
const container = shallowRef<Document | HTMLElement>();
const visible = ref(false);
const handleScroll = () => {
if (el.value) {
visible.value = el.value.scrollTop >= (props?.visibilityHeight ?? 0);
}
};
const handleClick = () => {
el.value?.scrollTo({ behavior: 'smooth', top: 0 });
};
const handleScrollThrottled = useThrottleFn(handleScroll, 300, true);
useEventListener(container, 'scroll', handleScrollThrottled);
onMounted(() => {
container.value = document;
el.value = document.documentElement;
if (props.target) {
el.value = document.querySelector<HTMLElement>(props.target) ?? undefined;
if (!el.value) {
throw new Error(`target does not exist: ${props.target}`);
}
container.value = el.value;
}
// Give visible an initial value, fix #13066
handleScroll();
});
return {
handleClick,
visible,
};
};

View File

@@ -0,0 +1,111 @@
<script lang="ts" setup>
import type { IBreadcrumb } from './interface';
import { VbenIcon } from '../icon';
interface Props {
breadcrumbs: IBreadcrumb[];
showIcon?: boolean;
}
defineOptions({ name: 'Breadcrumb' });
withDefaults(defineProps<Props>(), {
showIcon: false,
});
const emit = defineEmits<{ select: [string] }>();
function handleClick(path?: string) {
if (!path) {
return;
}
emit('select', path);
}
</script>
<template>
<ul class="flex">
<TransitionGroup name="breadcrumb-transition">
<template
v-for="(item, index) in breadcrumbs"
:key="`${item.path}-${item.title}-${index}`"
>
<li>
<a href="javascript:void 0" @click.stop="handleClick(item.path)">
<span class="flex-center h-full">
<VbenIcon
v-if="item.icon && showIcon"
:icon="item.icon"
class="mr-1 size-5 flex-shrink-0"
/>
<span
:class="{
'text-foreground font-normal':
index === breadcrumbs.length - 1,
}"
>{{ item.title }}
</span>
</span>
</a>
</li>
</template>
</TransitionGroup>
</ul>
</template>
<style scoped>
li {
@apply h-7;
}
li a {
@apply text-muted-foreground bg-accent relative mr-9 flex h-7 items-center py-0 pl-[5px] pr-2 text-[13px];
}
li a > span {
@apply -ml-3;
}
li:first-child a > span {
@apply -ml-1;
}
li:first-child a {
@apply rounded-[4px_0_0_4px] pl-[15px];
}
li:first-child a::before {
@apply border-none;
}
li:last-child a {
@apply rounded-[0_4px_4px_0] pr-[15px];
}
li:last-child a::after {
@apply border-none;
}
li a::before,
li a::after {
@apply border-accent absolute top-0 h-0 w-0 border-[14px] border-solid content-[''];
}
li a::before {
@apply -left-7 z-10 border-l-transparent;
}
li a::after {
@apply border-l-accent left-full border-transparent;
}
li:not(:last-child) a:hover {
@apply bg-accent-hover;
}
li:not(:last-child) a:hover::before {
@apply border-accent-hover border-l-transparent;
}
li:not(:last-child) a:hover::after {
@apply border-l-accent-hover;
}
</style>

View File

@@ -0,0 +1,108 @@
<script lang="ts" setup>
import type { IBreadcrumb } from './interface';
import { IcRoundKeyboardArrowDown } from '@vben-core/iconify';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@vben-core/shadcn-ui/components/ui/breadcrumb';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@vben-core/shadcn-ui/components/ui/dropdown-menu';
import { VbenIcon } from '../icon';
interface Props {
breadcrumbs: IBreadcrumb[];
showIcon?: boolean;
}
defineOptions({ name: 'Breadcrumb' });
withDefaults(defineProps<Props>(), {
showIcon: false,
});
const emit = defineEmits<{ select: [string] }>();
function handleClick(path?: string) {
if (!path) {
return;
}
emit('select', path);
}
</script>
<template>
<Breadcrumb>
<BreadcrumbList>
<TransitionGroup name="breadcrumb-transition">
<template
v-for="(item, index) in breadcrumbs"
:key="`${item.path}-${item.title}-${index}`"
>
<BreadcrumbItem>
<div v-if="item.items?.length ?? 0 > 0">
<DropdownMenu>
<DropdownMenuTrigger class="flex items-center gap-1">
<VbenIcon
v-if="item.icon && showIcon"
:icon="item.icon"
class="size-5"
/>
{{ item.title }}
<IcRoundKeyboardArrowDown class="size-5" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<template
v-for="menuItem in item.items"
:key="`sub-${menuItem.path}`"
>
<DropdownMenuItem @click.stop="handleClick(menuItem.path)">
{{ menuItem.title }}
</DropdownMenuItem>
</template>
</DropdownMenuContent>
</DropdownMenu>
</div>
<BreadcrumbLink
v-else-if="index !== breadcrumbs.length - 1"
href="javascript:void 0"
@click.stop="handleClick(item.path)"
>
<div class="flex-center">
<VbenIcon
v-if="item.icon && showIcon"
:class="{ 'size-5': item.isHome }"
:icon="item.icon"
class="mr-1 size-4"
/>
{{ item.title }}
</div>
</BreadcrumbLink>
<BreadcrumbPage v-else>
<div class="flex-center">
<VbenIcon
v-if="item.icon && showIcon"
:class="{ 'size-5': item.isHome }"
:icon="item.icon"
class="mr-1 size-4"
/>
{{ item.title }}
</div>
</BreadcrumbPage>
<BreadcrumbSeparator
v-if="index < breadcrumbs.length - 1 && !item.isHome"
/>
</BreadcrumbItem>
</template>
</TransitionGroup>
</BreadcrumbList>
</Breadcrumb>
</template>

View File

@@ -0,0 +1,4 @@
export { default as VbenBreadcrumb } from './breadcrumb.vue';
export { default as VbenBackgroundBreadcrumb } from './breadcrumb-background.vue';
export type * from './interface';

View File

@@ -0,0 +1,9 @@
interface IBreadcrumb {
icon?: string;
isHome?: boolean;
items?: IBreadcrumb[];
path?: string;
title?: string;
}
export type { IBreadcrumb };

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { MdiLoading } from '@vben-core/iconify';
import { cn } from '@vben-core/toolkit';
import {
type ButtonVariants,
buttonVariants,
} from '@vben-core/shadcn-ui/components/ui/button';
import { Primitive, type PrimitiveProps } from 'radix-vue';
interface Props extends PrimitiveProps {
class?: HTMLAttributes['class'];
disabled?: boolean;
loading?: boolean;
size?: ButtonVariants['size'];
variant?: ButtonVariants['variant'];
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
class: '',
disabled: false,
loading: false,
size: 'default',
variant: 'default',
});
const isDisabled = computed(() => {
return props.disabled || props.loading;
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
:disabled="isDisabled"
>
<MdiLoading
v-if="loading"
class="text-md mr-2 flex-shrink-0 animate-spin"
/>
<slot></slot>
</Primitive>
</template>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { type HTMLAttributes, computed, useSlots } from 'vue';
import { cn } from '@vben-core/toolkit';
import { VbenTooltip } from '@vben-core/shadcn-ui/components/tooltip';
import { ButtonVariants } from '@vben-core/shadcn-ui/components/ui/button';
import { type PrimitiveProps } from 'radix-vue';
import VbenButton from './button.vue';
interface Props extends PrimitiveProps {
class?: HTMLAttributes['class'];
disabled?: boolean;
onClick?: () => void;
tooltip?: string;
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
variant?: ButtonVariants['variant'];
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
onClick: () => {},
tooltipSide: 'bottom',
variant: 'icon',
});
const slots = useSlots();
const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
</script>
<template>
<VbenButton
v-if="!showTooltip"
:class="cn('rounded-full', props.class)"
:disabled="disabled"
:variant="variant"
size="icon"
@click="onClick"
>
<slot></slot>
</VbenButton>
<VbenTooltip v-else :side="tooltipSide">
<template #trigger>
<VbenButton
:class="cn('rounded-full', props.class)"
:disabled="disabled"
:variant="variant"
size="icon"
@click="onClick"
>
<slot></slot>
</VbenButton>
</template>
<slot v-if="slots.tooltip" name="tooltip"> </slot>
<template v-else>
{{ tooltip }}
</template>
</VbenTooltip>
</template>

View File

@@ -0,0 +1,2 @@
export { default as VbenButton } from './button.vue';
export { default as VbenIconButton } from './icon-button.vue';

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue';
import { Checkbox } from '@vben-core/shadcn-ui/components/ui/checkbox';
import { useForwardPropsEmits } from 'radix-vue';
const props = defineProps<
{
name: string;
} & CheckboxRootProps
>();
const emits = defineEmits<CheckboxRootEmits>();
const checked = defineModel<boolean>('checked');
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<Checkbox v-bind="forwarded" :id="name" v-model:checked="checked" />
<label :for="name" class="ml-2 cursor-pointer text-sm"> <slot></slot> </label>
</template>

View File

@@ -0,0 +1 @@
export { default as VbenCheckbox } from './checkbox.vue';

View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
import type {
ContextMenuContentProps,
ContextMenuRootEmits,
ContextMenuRootProps,
} from 'radix-vue';
import type { IContextMenuItem } from './interface';
import type { HTMLAttributes } from 'vue';
import { computed } from 'vue';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from '@vben-core/shadcn-ui/components/ui/context-menu';
import { useForwardPropsEmits } from 'radix-vue';
const props = defineProps<
{
class?: HTMLAttributes['class'];
contentClass?: HTMLAttributes['class'];
contentProps?: ContextMenuContentProps;
handlerData?: Record<string, any>;
itemClass?: HTMLAttributes['class'];
menus: (data: any) => IContextMenuItem[];
} & ContextMenuRootProps
>();
const emits = defineEmits<ContextMenuRootEmits>();
const delegatedProps = computed(() => {
const {
class: _cls,
contentClass: _,
contentProps: _cProps,
itemClass: _iCls,
...delegated
} = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const menusView = computed(() => {
return props.menus?.(props.handlerData);
});
function handleClick(menu: IContextMenuItem) {
if (menu.disabled) {
return;
}
menu?.handler?.(props.handlerData);
}
</script>
<template>
<ContextMenu v-bind="forwarded">
<ContextMenuTrigger as-child>
<slot></slot>
</ContextMenuTrigger>
<ContextMenuContent
:class="contentClass"
v-bind="contentProps"
class="side-content z-[1000]"
>
<template v-for="menu in menusView" :key="menu.key">
<ContextMenuItem
:class="itemClass"
:disabled="menu.disabled"
:inset="menu.inset || !menu.icon"
class="cursor-pointer"
@click="handleClick(menu)"
>
<component
:is="menu.icon"
v-if="menu.icon"
class="mr-1 w-6 text-lg"
/>
{{ menu.text }}
<ContextMenuShortcut v-if="menu.shortcut">
{{ menu.shortcut }}
</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSeparator v-if="menu.separator" />
</template>
</ContextMenuContent>
</ContextMenu>
</template>

View File

@@ -0,0 +1,3 @@
export { default as VbenContextMenu } from './context-menu.vue';
export type * from './interface';

View File

@@ -0,0 +1,38 @@
import type { Component } from 'vue';
interface IContextMenuItem {
/**
* @zh_CN 是否禁用
*/
disabled?: boolean;
/**
* @zh_CN 点击事件处理
* @param data
*/
handler?: (data: any) => void;
/**
* @zh_CN 图标
*/
icon?: Component;
/**
* @zh_CN 是否显示图标
*/
inset?: boolean;
/**
* @zh_CN 唯一标识
*/
key: string;
/**
* @zh_CN 是否是分割线
*/
separator?: boolean;
/**
* @zh_CN 快捷键
*/
shortcut?: string;
/**
* @zh_CN 标题
*/
text: string;
}
export type { IContextMenuItem };

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import type {
DropdownMenuProps,
VbenDropdownMenuItem as IDropdownMenuItem,
} from './interface';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@vben-core/shadcn-ui/components/ui/dropdown-menu';
interface Props extends DropdownMenuProps {}
defineOptions({ name: 'DropdownMenu' });
const props = withDefaults(defineProps<Props>(), {});
function handleItemClick(menu: IDropdownMenuItem) {
if (menu.disabled) {
return;
}
menu?.handler?.(props);
}
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger class="flex h-full items-center gap-1">
<slot></slot>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuGroup>
<template v-for="menu in menus" :key="menu.key">
<DropdownMenuItem
:disabled="menu.disabled"
class="data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground text-foreground/80 mb-1 cursor-pointer"
@click="handleItemClick(menu)"
>
<component :is="menu.icon" v-if="menu.icon" class="mr-2 size-4" />
{{ menu.text }}
</DropdownMenuItem>
<DropdownMenuSeparator v-if="menu.separator" class="bg-border" />
</template>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@@ -0,0 +1,50 @@
<script lang="ts" setup>
import type { DropdownMenuProps } from './interface';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@vben-core/shadcn-ui/components/ui/dropdown-menu';
interface Props extends DropdownMenuProps {}
defineOptions({ name: 'DropdownRadioMenu' });
withDefaults(defineProps<Props>(), {});
const modelValue = defineModel<string>();
function handleItemClick(value: string) {
modelValue.value = value;
}
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child class="flex items-center gap-1">
<slot></slot>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuGroup>
<template v-for="menu in menus" :key="menu.key">
<DropdownMenuItem
:class="
menu.key === modelValue ? 'bg-accent text-accent-foreground' : ''
"
class="data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground text-foreground/80 mb-1 cursor-pointer"
@click="handleItemClick(menu.key)"
>
<component :is="menu.icon" v-if="menu.icon" class="mr-2 size-4" />
<span
v-if="!menu.icon"
:class="menu.key === modelValue ? 'bg-foreground' : ''"
class="mr-2 size-1.5 rounded-full"
></span>
{{ menu.text }}
</DropdownMenuItem>
</template>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@@ -0,0 +1,4 @@
export { default as VbenDropdownMenu } from './dropdown-menu.vue';
export { default as VbenDropdownRadioMenu } from './dropdown-radio-menu.vue';
export type * from './interface';

View File

@@ -0,0 +1,32 @@
import type { Component } from 'vue';
interface VbenDropdownMenuItem {
disabled?: boolean;
/**
* @zh_CN 点击事件处理
* @param data
*/
handler?: (data: any) => void;
/**
* @zh_CN 图标
*/
icon?: Component;
/**
* @zh_CN 唯一标识
*/
key: string;
/**
* @zh_CN 是否是分割线
*/
separator?: boolean;
/**
* @zh_CN 标题
*/
text: string;
}
interface DropdownMenuProps {
menus: VbenDropdownMenuItem[];
}
export type { DropdownMenuProps, VbenDropdownMenuItem };

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { ref } from 'vue';
const isMenuOpen = ref(false);
const menuItems = ref(['1', '2', '3', '4']);
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
};
const handleMenuItemClick = (_item: any) => {
// console.log(111, item);
};
</script>
<template>
<div class="fixed bottom-5 right-5 flex flex-col-reverse items-center gap-2">
<button
:class="{ 'rotate-45': isMenuOpen }"
class="flex h-12 w-12 items-center justify-center rounded-full bg-blue-500 text-xl text-white transition-transform duration-300"
@click="toggleMenu"
>
</button>
<div
:class="{
'visible translate-y-0 opacity-100': isMenuOpen,
'invisible translate-y-2 opacity-0': !isMenuOpen,
}"
class="absolute bottom-16 right-0 flex flex-col-reverse gap-2 transition-all duration-300"
>
<button
v-for="(item, index) in menuItems"
:key="index"
class="flex h-12 w-12 items-center justify-center rounded-full bg-blue-500 text-xl text-white"
@click="handleMenuItemClick(item)"
>
{{ item }}
</button>
</div>
</div>
</template>
<style scoped>
/* 可以在这里添加任何需要的额外样式 */
</style>

View File

@@ -0,0 +1 @@
export { default as VbenFloatingButtonGroup } from './floating-button-group.vue';

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import { IcRoundFullscreen, IcRoundFullscreenExit } from '@vben-core/iconify';
import { useFullscreen } from '@vueuse/core';
import { VbenIconButton } from '../button';
defineOptions({ name: 'FullScreen' });
const { isFullscreen, toggle } = useFullscreen();
// 重新检查全屏状态
isFullscreen.value = !!(
document.fullscreenElement ||
// @ts-ignore
document.webkitFullscreenElement ||
// @ts-ignore
document.mozFullScreenElement ||
// @ts-ignore
document.msFullscreenElement
);
</script>
<template>
<VbenIconButton @click="toggle">
<IcRoundFullscreenExit v-if="isFullscreen" class="size-6" />
<IcRoundFullscreen v-else class="size-6" />
</VbenIconButton>
</template>

View File

@@ -0,0 +1 @@
export { default as VbenFullScreen } from './full-screen.vue';

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import type { HoverCardRootEmits, HoverCardRootProps } from 'radix-vue';
import { HTMLAttributes, computed } from 'vue';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@vben-core/shadcn-ui/components/ui/hover-card';
import { HoverCardContentProps, useForwardPropsEmits } from 'radix-vue';
const props = defineProps<
{
class?: HTMLAttributes['class'];
contentClass?: HTMLAttributes['class'];
contentProps?: HoverCardContentProps;
} & HoverCardRootProps
>();
const emits = defineEmits<HoverCardRootEmits>();
const delegatedProps = computed(() => {
const {
class: _cls,
contentClass: _,
contentProps: _cProps,
...delegated
} = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<HoverCard v-bind="forwarded">
<HoverCardTrigger as-child class="h-full">
<div class="h-full cursor-pointer">
<slot name="trigger"></slot>
</div>
</HoverCardTrigger>
<HoverCardContent
:class="contentClass"
v-bind="contentProps"
class="side-content z-[1000]"
>
<slot></slot>
</HoverCardContent>
</HoverCard>
</template>

View File

@@ -0,0 +1,2 @@
export { default as VbenHoverCard } from './hover-card.vue';
export type { HoverCardContentProps } from 'radix-vue';

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import { type Component, computed } from 'vue';
import { Icon, IconDefault } from '@vben-core/iconify';
import { isHttpUrl, isObject, isString } from '@vben-core/toolkit';
const props = defineProps<{
// 没有是否显示默认图标
fallback?: boolean;
icon?: Component | string;
}>();
const isRemoteIcon = computed(() => {
return isString(props.icon) && isHttpUrl(props.icon);
});
const isComponent = computed(() => {
return !isString(props.icon) && isObject(props.icon);
});
</script>
<template>
<component :is="icon as Component" v-if="isComponent" v-bind="$attrs" />
<img v-else-if="isRemoteIcon" :src="icon as string" v-bind="$attrs" />
<Icon v-else-if="icon" v-bind="$attrs" :icon="icon as string" />
<IconDefault v-else-if="fallback" v-bind="$attrs" />
</template>

View File

@@ -0,0 +1 @@
export { default as VbenIcon } from './icon.vue';

View File

@@ -0,0 +1,44 @@
// 修改过的button
export * from './alert-dialog';
export * from './avatar';
export * from './back-top';
export * from './breadcrumb';
export * from './button';
export * from './checkbox';
export * from './context-menu';
export * from './dropdown-menu';
export * from './floating-button-group';
export * from './full-screen';
export * from './hover-card';
export * from './icon';
export * from './input';
export * from './input-password';
export * from './logo';
export * from './menu-badge';
export * from './pin-input';
export * from './popover';
export * from './scrollbar';
export * from './segmented';
export * from './sheet';
export * from './spinner';
export * from './tooltip';
export * from './ui/alert-dialog';
export * from './ui/avatar';
export * from './ui/badge';
export * from './ui/breadcrumb';
export * from './ui/button';
export * from './ui/checkbox';
export * from './ui/dialog';
export * from './ui/dropdown-menu';
export * from './ui/hover-card';
export * from './ui/pin-input';
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/toggle';
export * from './ui/toggle-group';
export * from './ui/tooltip';

View File

@@ -0,0 +1 @@
export { default as VbenInputPassword } from './input-password.vue';

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { ref, useSlots } from 'vue';
import {
IcOutlineVisibility,
IcOutlineVisibilityOff,
} from '@vben-core/iconify';
import {
type InputProps,
VbenInput,
} from '@vben-core/shadcn-ui/components/input';
import { useForwardProps } from 'radix-vue';
import PasswordStrength from './password-strength.vue';
interface Props extends InputProps {}
defineOptions({
inheritAttrs: false,
});
const props = defineProps<Props>();
const modelValue = defineModel<string>();
const slots = useSlots();
const forward = useForwardProps(props);
const show = ref(false);
</script>
<template>
<form class="relative">
<VbenInput
v-model="modelValue"
v-bind="{ ...forward, ...$attrs }"
:type="show ? 'text' : 'password'"
>
<template v-if="passwordStrength">
<PasswordStrength :password="modelValue" />
<p
v-if="slots.strengthText"
class="text-muted-foreground mt-1.5 text-xs"
>
<slot name="strengthText"> </slot>
</p>
</template>
</VbenInput>
<div
class="hover:text-foreground text-foreground/60 absolute inset-y-0 right-0 top-3 flex cursor-pointer pr-3 text-lg leading-5"
@click="show = !show"
>
<IcOutlineVisibility v-if="show" />
<IcOutlineVisibilityOff v-else />
</div>
</form>
</template>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = withDefaults(defineProps<{ password?: string }>(), {
password: '',
});
const strengthList: string[] = [
'',
'#e74242',
'#ED6F6F',
'#EFBD47',
'#55D18780',
'#55D187',
];
const currentStrength = computed(() => {
return checkPasswordStrength(props.password);
});
const currentColor = computed(() => {
return strengthList[currentStrength.value];
});
/**
* Check the strength of a password
*/
function checkPasswordStrength(password: string) {
let strength = 0;
// Check length
if (password.length >= 8) strength++;
// Check for lowercase letters
if (/[a-z]/.test(password)) strength++;
// Check for uppercase letters
if (/[A-Z]/.test(password)) strength++;
// Check for numbers
if (/\d/.test(password)) strength++;
// Check for special characters
if (/[^\da-z]/i.test(password)) strength++;
return strength;
}
</script>
<template>
<div class="relative mt-2 flex items-center justify-between">
<template v-for="index in 5" :key="index">
<div
class="dark:bg-input-background bg-heavy relative mr-1 h-1.5 w-1/5 rounded-sm last:mr-0"
>
<span
:style="{
backgroundColor: currentColor,
width: currentStrength >= index ? '100%' : '',
}"
class="absolute left-0 h-full w-0 rounded-sm transition-all duration-500"
></span>
</div>
</template>
</div>
</template>

View File

@@ -0,0 +1,2 @@
export { default as VbenInput } from './input.vue';
export type * from './interface';

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import type { InputProps } from './interface';
import { computed } from 'vue';
defineOptions({
inheritAttrs: false,
});
const props = defineProps<InputProps>();
const modelValue = defineModel<number | string>();
const inputClass = computed(() => {
if (props.status === 'error') {
return 'border-destructive';
}
return '';
});
</script>
<template>
<div class="relative mb-6">
<label
v-if="!label"
:for="name"
class="mb-2 block text-sm font-medium dark:text-white"
>
{{ label }}
</label>
<input
:id="name"
v-model="modelValue"
:class="[props.class, inputClass]"
autocomplete="off"
class="border-input bg-input-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring focus:border-primary flex h-10 w-full rounded-md border p-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
required
type="text"
v-bind="$attrs"
/>
<slot></slot>
<Transition name="slide-up">
<p
v-if="status === 'error'"
class="text-destructive bottom-130 absolute mt-1 text-xs"
>
{{ errorTip }}
</p>
</Transition>
</div>
</template>

View File

@@ -0,0 +1,27 @@
import type { HTMLAttributes } from 'vue';
interface InputProps {
class?: HTMLAttributes['class'];
/**
* 错误提示信息
*/
errorTip?: string;
/**
* 输入框的 label
*/
label?: string;
/**
* 输入框的 name
*/
name?: string;
/**
* 是否显示密码强度
*/
passwordStrength?: boolean;
/**
* 输入框的校验状态
*/
status?: 'default' | 'error';
}
export type { InputProps };

View File

@@ -0,0 +1 @@
export { default as VbenLogo } from './logo.vue';

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
/**
* Logo 图标 alt
*/
alt?: string;
/**
* 是否收起文本
*/
collapse?: boolean;
/**
* Logo 跳转地址
*/
href?: string;
/**
* Logo 图片大小
*/
logoSize?: number;
/**
* Logo 图标
*/
src?: string;
/**
* Logo 文本
*/
text?: string;
/**
* Logo 主题
*/
theme?: string;
}
defineOptions({
name: 'Logo',
});
const props = withDefaults(defineProps<Props>(), {
alt: 'Vben',
collapse: false,
href: 'javascript:void 0',
logoSize: 36,
src: '',
text: '',
theme: 'light',
});
const logoClass = computed(() => {
return [props.theme, props.collapse ? 'collapsed' : ''];
});
</script>
<template>
<div :class="logoClass" class="group flex h-full items-center text-lg">
<a
:class="$attrs.class"
:href="href"
class="flex h-full items-center gap-2 overflow-hidden px-3 font-semibold leading-normal transition-all duration-500"
>
<img
v-if="src"
:alt="alt"
:src="src"
:width="logoSize"
class="relative rounded-none bg-transparent"
/>
<span
v-if="!collapse"
class="text-primary truncate text-nowrap group-[.dark]:text-[hsl(var(--dark-foreground))]"
>
{{ text }}
<!-- <span class="text-primary ml-1 align-super text-[smaller]">Pro</span> -->
</span>
</a>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as VbenMenuBadge } from './menu-badge.vue';

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue';
interface Props {
dotClass?: string;
dotStyle?: CSSProperties;
}
withDefaults(defineProps<Props>(), {
dotClass: '',
dotStyle: () => ({}),
});
</script>
<template>
<span class="relative mr-1 flex size-1.5">
<span
:class="dotClass"
:style="dotStyle"
class="absolute inline-flex h-full w-full animate-ping rounded-full opacity-75"
>
</span>
<span
:class="dotClass"
:style="dotStyle"
class="relative inline-flex size-1.5 rounded-full"
></span>
</span>
</template>

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import type { MenuRecordBadgeRaw } from '@vben-core/typings';
import { computed } from 'vue';
import { isValidColor } from '@vben-core/colorful';
import BadgeDot from './menu-badge-dot.vue';
interface Props extends MenuRecordBadgeRaw {
hasChildren?: boolean;
}
const props = withDefaults(defineProps<Props>(), {});
const variantsMap: Record<string, string> = {
default: 'bg-green-500',
destructive: ' bg-destructive',
primary: 'bg-primary',
success: ' bg-green-500',
warning: ' bg-yellow-500',
};
const isDot = computed(() => props.badgeType === 'dot');
const badgeClass = computed(() => {
const { badgeVariants } = props;
if (!badgeVariants) {
return variantsMap.default;
}
return variantsMap[badgeVariants] || badgeVariants;
});
const badgeStyle = computed(() => {
if (badgeClass.value && isValidColor(badgeClass.value)) {
return {
backgroundColor: badgeClass.value,
};
}
return {};
});
</script>
<template>
<span v-if="isDot || badge" :class="$attrs.class" class="absolute right-5">
<BadgeDot v-if="isDot" :dot-class="badgeClass" :dot-style="badgeStyle" />
<div
v-else
:class="badgeClass"
:style="badgeStyle"
class="rounded-md px-1.5 py-0.5 text-xs"
>
{{ badge }}
</div>
</span>
</template>

View File

@@ -0,0 +1,3 @@
export { default as VbenPinInput } from './input.vue';
export type * from './interface';

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import type { PinInputProps } from './interface';
import { computed, ref, watch } from 'vue';
import { VbenButton } from '@vben-core/shadcn-ui/components/button';
import {
PinInput,
PinInputGroup,
PinInputInput,
} from '@vben-core/shadcn-ui/components/ui/pin-input';
defineOptions({
inheritAttrs: false,
});
const props = withDefaults(defineProps<PinInputProps>(), {
btnLoading: false,
codeLength: 6,
handleSendCode: async () => {},
});
const emit = defineEmits<{
complete: [];
}>();
const modelValue = defineModel<string>();
const inputValue = ref<string[]>([]);
const inputClass = computed(() => {
if (props.status === 'error') {
return 'border-destructive';
}
return '';
});
watch(
() => modelValue.value,
() => {
inputValue.value = modelValue.value?.split('') ?? [];
},
);
function handleComplete(e: string[]) {
modelValue.value = e.join('');
emit('complete');
}
</script>
<template>
<div class="relative mb-6">
<label :for="name" class="mb-2 block text-sm font-medium">
{{ label }}
</label>
<PinInput
:id="name"
v-model="inputValue"
:class="inputClass"
class="flex justify-between"
otp
placeholder="○"
type="number"
@complete="handleComplete"
>
<PinInputGroup>
<PinInputInput
v-for="(id, index) in codeLength"
:key="id"
:index="index"
/>
</PinInputGroup>
<VbenButton
:loading="btnLoading"
class="w-[300px] xl:w-full"
size="lg"
variant="outline"
@click="handleSendCode"
>
{{ btnText }}
</VbenButton>
</PinInput>
<p
v-if="status === 'error'"
class="text-destructive bottom-130 absolute mt-1 text-xs"
>
{{ errorTip }}
</p>
</div>
</template>

View File

@@ -0,0 +1,40 @@
import type { HTMLAttributes } from 'vue';
interface PinInputProps {
/**
* 发送验证码按钮loading
*/
btnLoading?: boolean;
/**
* 发送验证码按钮文本
*/
btnText?: string;
class?: HTMLAttributes['class'];
/**
* 验证码长度
*/
codeLength?: number;
/**
* 错误提示信息
*/
errorTip?: string;
/**
* 自定义验证码发送逻辑
* @returns
*/
handleSendCode?: () => Promise<void>;
/**
* 输入框的 label
*/
label: string;
/**
* 输入框的 name
*/
name: string;
/**
* 输入框的校验状态
*/
status?: 'default' | 'error';
}
export type { PinInputProps };

View File

@@ -0,0 +1 @@
export { default as VbenPopover } from './popover.vue';

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import type {
PopoverContentProps,
PopoverRootEmits,
PopoverRootProps,
} from 'radix-vue';
import { HTMLAttributes, computed } from 'vue';
import {
PopoverContent,
Popover as PopoverRoot,
PopoverTrigger,
} from '@vben-core/shadcn-ui/components/ui/popover';
import { useForwardPropsEmits } from 'radix-vue';
const props = withDefaults(
defineProps<
{
class?: HTMLAttributes['class'];
contentClass?: HTMLAttributes['class'];
contentProps?: PopoverContentProps;
} & PopoverRootProps
>(),
{},
);
const emits = defineEmits<PopoverRootEmits>();
const delegatedProps = computed(() => {
const {
class: _cls,
contentClass: _,
contentProps: _cProps,
...delegated
} = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<PopoverRoot v-bind="forwarded">
<PopoverTrigger>
<slot name="trigger"></slot>
<PopoverContent
:class="contentClass"
class="side-content z-[1000]"
v-bind="contentProps"
>
<slot></slot>
</PopoverContent>
</PopoverTrigger>
</PopoverRoot>
</template>

View File

@@ -0,0 +1 @@
export { default as VbenScrollbar } from './scrollbar.vue';

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { ref } from 'vue';
import { cn } from '@vben-core/toolkit';
import { ScrollArea } from '@vben-core/shadcn-ui/components/ui/scroll-area';
interface Props {
class?: HTMLAttributes['class'];
}
const props = withDefaults(defineProps<Props>(), {
class: '',
});
const isAtTop = ref(true);
const isAtBottom = ref(false);
function handleScroll(event: Event) {
const target = event.target as HTMLElement;
const scrollTop = target?.scrollTop ?? 0;
const offsetHeight = target?.offsetHeight ?? 0;
const scrollHeight = target?.scrollHeight ?? 0;
isAtTop.value = scrollTop <= 0;
isAtBottom.value = scrollTop + offsetHeight >= scrollHeight;
}
</script>
<template>
<ScrollArea
:class="[
cn(props.class),
{
// 'shadow-none': isAtTop && isAtBottom,
// shadow: !isAtTop || !isAtBottom,
// 'dark:shadow-white/20': !isAtTop || !isAtBottom,
// 'shadow-inner': !isAtBottom,
// 'dark:shadow-inner-white/20': !isAtBottom,
},
]"
:on-scroll="handleScroll"
>
<slot></slot>
</ScrollArea>
</template>

View File

@@ -0,0 +1,3 @@
export type * from './interface';
export { default as VbenSegmented } from './segmented.vue';

View File

@@ -0,0 +1,6 @@
interface SegmentedItem {
label: string;
value: string;
}
export type { SegmentedItem };

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import type { SegmentedItem } from './interface';
import { computed } from 'vue';
import {
Tabs,
TabsContent,
TabsList,
} from '@vben-core/shadcn-ui/components/ui/tabs';
import { TabsTrigger } from 'radix-vue';
import TabsIndicator from './tabs-indicator.vue';
interface Props {
defaultValue?: string;
tabs: SegmentedItem[];
}
const props = withDefaults(defineProps<Props>(), {
defaultValue: '',
tabs: () => [],
});
const activeTab = defineModel<string>();
const getDefaultValue = computed(() => {
return props.defaultValue || props.tabs[0]?.value;
});
const tabsStyle = computed(() => {
return {
'grid-template-columns': `repeat(${props.tabs.length}, minmax(0, 1fr))`,
};
});
const tabsIndicatorStyle = computed(() => {
return {
width: `${(100 / props.tabs.length).toFixed(0)}%`,
};
});
</script>
<template>
<Tabs v-model="activeTab" :default-value="getDefaultValue">
<TabsList :style="tabsStyle" class="bg-accent relative grid w-full">
<TabsIndicator :style="tabsIndicatorStyle" />
<template v-for="tab in tabs" :key="tab.value">
<TabsTrigger
:value="tab.value"
class="z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50"
>
{{ tab.label }}
</TabsTrigger>
</template>
</TabsList>
<template v-for="tab in tabs" :key="tab.value">
<TabsContent :value="tab.value">
<slot :name="tab.value"></slot>
</TabsContent>
</template>
</Tabs>
</template>

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue';
import { cn } from '@vben-core/toolkit';
import {
TabsIndicator,
type TabsIndicatorProps,
useForwardProps,
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & TabsIndicatorProps
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<TabsIndicator
v-bind="forwardedProps"
:class="
cn(
'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-1 transition-[width,transform] duration-300',
props.class,
)
"
>
<div
class="bg-background text-foreground inline-flex h-full w-full items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
>
<slot></slot>
</div>
</TabsIndicator>
</template>

View File

@@ -0,0 +1 @@
export { default as VbenSheet } from './sheet.vue';

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { computed, useSlots } from 'vue';
import { Cross2Icon } from '@radix-icons/vue';
import {
VbenButton,
VbenIconButton,
} from '@vben-core/shadcn-ui/components/button';
import { VbenScrollbar } from '@vben-core/shadcn-ui/components/scrollbar';
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@vben-core/shadcn-ui/components/ui/sheet';
interface Props {
cancelText?: string;
description?: string;
showFooter?: boolean;
submitText?: string;
title?: string;
width?: number;
}
const props = withDefaults(defineProps<Props>(), {
cancelText: '关闭',
description: '',
showFooter: false,
submitText: '确认',
title: '',
width: 400,
});
const emits = defineEmits<{
cancel: [];
submit: [];
}>();
const openModal = defineModel<boolean>('open');
const slots = useSlots();
const contentStyle = computed(() => {
return {
width: `${props.width}px`,
};
});
function handlerSubmit() {
emits('submit');
openModal.value = false;
}
// function handleCancel() {
// emits('cancel');
// openModal.value = false;
// }
</script>
<template>
<Sheet v-model:open="openModal">
<SheetTrigger>
<slot name="trigger"></slot>
</SheetTrigger>
<SheetContent :style="contentStyle" class="!w-full pb-12 sm:rounded-l-lg">
<SheetHeader
:class="description ? 'h-16' : 'h-12'"
class="border-border flex flex-row items-center justify-between border-b pl-3 pr-3"
>
<div class="flex w-full items-center justify-between">
<div>
<SheetTitle class="text-left text-lg">{{ title }}</SheetTitle>
<SheetDescription class="text-muted-foreground text-xs">
{{ description }}
</SheetDescription>
</div>
<slot v-if="slots.extra" name="extra"></slot>
</div>
<SheetClose
as-child
class="data-[state=open]:bg-secondary cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
>
<VbenIconButton>
<Cross2Icon class="size-4" />
</VbenIconButton>
</SheetClose>
</SheetHeader>
<div class="h-full pb-16">
<VbenScrollbar class="h-full">
<slot></slot>
</VbenScrollbar>
</div>
<SheetFooter v-if="showFooter || slots.footer" as-child>
<div
class="border-border absolute bottom-0 flex h-12 w-full items-center justify-end border-t"
>
<slot v-if="slots.footer" name="footer"></slot>
<template v-else>
<SheetClose as-child>
<VbenButton class="mr-2" variant="outline">
{{ cancelText }}
</VbenButton>
</SheetClose>
<VbenButton @click="handlerSubmit">{{ submitText }}</VbenButton>
</template>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
</template>

View File

@@ -0,0 +1 @@
export { default as Spinner } from './spinner.vue';

View File

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
interface Props {
/**
* @zh_CN 最小加载时间
* @en_US Minimum loading time
*/
minLoadingTime?: number;
/**
* @zh_CN loading状态开启
*/
spinning?: boolean;
}
defineOptions({
name: 'Spinner',
});
const props = withDefaults(defineProps<Props>(), {
minLoadingTime: 50,
});
// const startTime = ref(0);
const showSpinner = ref(false);
const renderSpinner = ref(true);
const timer = ref<ReturnType<typeof setTimeout>>();
watch(
() => props.spinning,
(show) => {
if (!show) {
showSpinner.value = false;
clearTimeout(timer.value);
return;
}
// startTime.value = performance.now();
timer.value = setTimeout(() => {
// const loadingTime = performance.now() - startTime.value;
showSpinner.value = true;
if (showSpinner.value) {
renderSpinner.value = true;
}
}, props.minLoadingTime);
},
{
immediate: true,
},
);
function onTransitionEnd() {
if (!showSpinner.value) {
renderSpinner.value = false;
}
}
</script>
<template>
<div
:class="{
'invisible opacity-0': !showSpinner,
}"
class="flex-center bg-overlay absolute left-0 top-0 size-full backdrop-blur-sm transition-all duration-500"
@transitionend="onTransitionEnd"
>
<div
class="loader before:bg-primary/50 after:bg-primary relative h-12 w-12 before:absolute before:left-0 before:top-[60px] before:h-[5px] before:w-12 before:animate-[loader-shadow-ani_0.5s_linear_infinite] before:rounded-[50%] before:content-[''] after:absolute after:left-0 after:top-0 after:h-full after:w-full after:animate-[loader-jump-ani_0.5s_linear_infinite] after:rounded after:content-['']"
></div>
</div>
</template>
<style>
@keyframes loader-jump-ani {
15% {
border-bottom-right-radius: 3px;
}
25% {
transform: translateY(9px) rotate(22.5deg);
}
50% {
border-bottom-right-radius: 40px;
transform: translateY(18px) scale(1, 0.9) rotate(45deg);
}
75% {
transform: translateY(9px) rotate(67.5deg);
}
100% {
transform: translateY(0) rotate(90deg);
}
}
@keyframes loader-shadow-ani {
0%,
100% {
transform: scale(1, 1);
}
50% {
transform: scale(1.2, 1);
}
}
</style>

View File

@@ -0,0 +1 @@
export { default as VbenTooltip } from './tooltip.vue';

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@vben-core/shadcn-ui/components/ui/tooltip';
import { TooltipContentProps } from 'radix-vue';
interface Props {
delayDuration?: number;
side: TooltipContentProps['side'];
}
withDefaults(defineProps<Props>(), {
delayDuration: 0,
side: 'right',
});
</script>
<template>
<TooltipProvider :delay-duration="delayDuration">
<Tooltip>
<TooltipTrigger as-child tabindex="-1">
<slot name="trigger"></slot>
</TooltipTrigger>
<TooltipContent
:side="side"
class="side-content text-popover-foreground bg-popover"
>
<slot></slot>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
type AlertDialogEmits,
type AlertDialogProps,
AlertDialogRoot,
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<AlertDialogProps>();
const emits = defineEmits<AlertDialogEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AlertDialogRoot v-bind="forwarded">
<slot></slot>
</AlertDialogRoot>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue';
import { cn } from '@vben-core/toolkit';
import { buttonVariants } from '@vben-core/shadcn-ui/components/ui/button';
import { AlertDialogAction, type AlertDialogActionProps } from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & AlertDialogActionProps
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogAction
v-bind="delegatedProps"
:class="cn(buttonVariants(), props.class)"
>
<slot></slot>
</AlertDialogAction>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue';
import { cn } from '@vben-core/toolkit';
import { buttonVariants } from '@vben-core/shadcn-ui/components/ui/button';
import { AlertDialogCancel, type AlertDialogCancelProps } from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & AlertDialogCancelProps
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AlertDialogCancel
v-bind="delegatedProps"
:class="
cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)
"
>
<slot></slot>
</AlertDialogCancel>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue';
import { cn } from '@vben-core/toolkit';
import {
AlertDialogContent,
type AlertDialogContentEmits,
type AlertDialogContentProps,
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & AlertDialogContentProps
>();
const emits = defineEmits<AlertDialogContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<AlertDialogPortal>
<AlertDialogOverlay
class="bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[1000] backdrop-blur-sm"
/>
<AlertDialogContent
v-bind="forwarded"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] border-border fixed left-1/2 top-1/2 z-[1000] grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
props.class,
)
"
>
<slot></slot>
</AlertDialogContent>
</AlertDialogPortal>
</template>

View File

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

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class,
)
"
>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
>
<slot></slot>
</div>
</template>

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
export { default as AlertDialog } from './AlertDialog.vue';
export { default as AlertDialogAction } from './AlertDialogAction.vue';
export { default as AlertDialogCancel } from './AlertDialogCancel.vue';
export { default as AlertDialogContent } from './AlertDialogContent.vue';
export { default as AlertDialogDescription } from './AlertDialogDescription.vue';
export { default as AlertDialogFooter } from './AlertDialogFooter.vue';
export { default as AlertDialogHeader } from './AlertDialogHeader.vue';
export { default as AlertDialogTitle } from './AlertDialogTitle.vue';
export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue';

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
import { AvatarRoot } from 'radix-vue';
import { type AvatarVariants, avatarVariant } from './avatar';
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class'];
shape?: AvatarVariants['shape'];
size?: AvatarVariants['size'];
}>(),
{
shape: 'circle',
size: 'sm',
},
);
</script>
<template>
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
<slot></slot>
</AvatarRoot>
</template>

View File

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

View File

@@ -0,0 +1,9 @@
<script setup lang="ts">
import { AvatarImage, type AvatarImageProps } from 'radix-vue';
const props = defineProps<AvatarImageProps>();
</script>
<template>
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
</template>

View File

@@ -0,0 +1,20 @@
import { type VariantProps, cva } from 'class-variance-authority';
export const avatarVariant = cva(
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
{
variants: {
shape: {
circle: 'rounded-full',
square: 'rounded-md',
},
size: {
base: 'h-16 w-16 text-2xl',
lg: 'h-32 w-32 text-5xl',
sm: 'h-10 w-10 text-xs',
},
},
},
);
export type AvatarVariants = VariantProps<typeof avatarVariant>;

View File

@@ -0,0 +1,4 @@
export { default as Avatar } from './Avatar.vue';
export { default as AvatarFallback } from './AvatarFallback.vue';
export { default as AvatarImage } from './AvatarImage.vue';
export * from './avatar';

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
import { type BadgeVariants, badgeVariants } from './badge';
const props = defineProps<{
class?: HTMLAttributes['class'];
variant?: BadgeVariants['variant'];
}>();
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,23 @@
import { type VariantProps, cva } from 'class-variance-authority';
export const badgeVariants = cva(
'inline-flex items-center rounded-md border border-border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
defaultVariants: {
variant: 'default',
},
variants: {
variant: {
default:
'border-transparent bg-accent hover:bg-accent text-primary-foreground shadow',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive-hover',
outline: 'text-foreground',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
},
},
},
);
export type BadgeVariants = VariantProps<typeof badgeVariants>;

View File

@@ -0,0 +1,3 @@
export { default as Badge } from './Badge.vue';
export * from './badge';

View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<nav :class="props.class" aria-label="breadcrumb" role="navigation">
<slot></slot>
</nav>
</template>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
import { DotsHorizontalIcon } from '@radix-icons/vue';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<span
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
aria-hidden="true"
role="presentation"
>
<slot>
<DotsHorizontalIcon class="h-4 w-4" />
</slot>
<span class="sr-only">More</span>
</span>
</template>

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<li
:class="
cn('hover:text-foreground inline-flex items-center gap-1.5', props.class)
"
>
<slot></slot>
</li>
</template>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
import { Primitive, type PrimitiveProps } from 'radix-vue';
const props = withDefaults(
defineProps<{ class?: HTMLAttributes['class'] } & PrimitiveProps>(),
{
as: 'a',
},
);
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('hover:text-foreground transition-colors', props.class)"
>
<slot></slot>
</Primitive>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<ol
:class="
cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5',
props.class,
)
"
>
<slot></slot>
</ol>
</template>

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<span
:class="cn('text-foreground font-normal', props.class)"
aria-current="page"
aria-disabled="true"
role="link"
>
<slot></slot>
</span>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
import { ChevronRightIcon } from '@radix-icons/vue';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<li
:class="cn('[&>svg]:size-3.5', props.class)"
aria-hidden="true"
role="presentation"
>
<slot>
<ChevronRightIcon />
</slot>
</li>
</template>

View File

@@ -0,0 +1,7 @@
export { default as Breadcrumb } from './Breadcrumb.vue';
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue';
export { default as BreadcrumbItem } from './BreadcrumbItem.vue';
export { default as BreadcrumbLink } from './BreadcrumbLink.vue';
export { default as BreadcrumbList } from './BreadcrumbList.vue';
export { default as BreadcrumbPage } from './BreadcrumbPage.vue';
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue';

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
import { Primitive, type PrimitiveProps } from 'radix-vue';
import { type ButtonVariants, buttonVariants } from './button';
interface Props extends PrimitiveProps {
class?: HTMLAttributes['class'];
size?: ButtonVariants['size'];
variant?: 'heavy' & ButtonVariants['variant'];
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
class: '',
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot></slot>
</Primitive>
</template>

View File

@@ -0,0 +1,36 @@
import { type VariantProps, cva } from 'class-variance-authority';
export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
{
defaultVariants: {
size: 'default',
variant: 'default',
},
variants: {
size: {
default: 'h-9 px-4 py-2',
icon: 'h-8 w-8 rounded-sm px-1 text-lg',
lg: 'h-10 rounded-md px-8',
sm: 'h-8 rounded-md px-3 text-xs',
xs: 'h-8 w-8 rounded-sm px-1 text-xs',
},
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive-hover',
ghost: 'hover:bg-accent hover:text-accent-foreground',
heavy: 'hover:bg-heavy hover:text-heavy-foreground',
icon: 'hover:bg-accent hover:text-accent-foreground text-foreground/80',
link: 'text-primary underline-offset-4 hover:underline',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
},
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View File

@@ -0,0 +1,3 @@
export { default as Button } from './Button.vue';
export * from './button';

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
:class="
cn('bg-card text-card-foreground rounded-xl border shadow', props.class)
"
>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<p :class="cn('text-muted-foreground text-sm', props.class)">
<slot></slot>
</p>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<h3 :class="cn('font-semibold leading-none tracking-tight', props.class)">
<slot></slot>
</h3>
</template>

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