提交新版本

This commit is contained in:
luob
2025-12-23 17:14:38 +08:00
3632 changed files with 498895 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.5.9",
"#main": "./dist/index.mjs",
"#module": "./dist/index.mjs",
"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": "./src/index.ts",
"module": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./src/index.ts",
"//default": "./dist/index.mjs"
}
},
"publishConfig": {
"exports": {
".": {
"default": "./src/index.ts"
}
}
},
"dependencies": {
"@vben-core/composables": "workspace:*",
"@vben-core/icons": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "catalog:",
"class-variance-authority": "catalog:",
"lucide-vue-next": "catalog:",
"reka-ui": "catalog:",
"vee-validate": "catalog:",
"vue": "catalog:"
}
}

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import type {
AvatarFallbackProps,
AvatarImageProps,
AvatarRootProps,
} from 'reka-ui';
import type { CSSProperties } from 'vue';
import type { ClassType } from '@vben-core/typings';
import { computed } from 'vue';
import { Avatar, AvatarFallback, AvatarImage } from '../../ui';
interface Props extends AvatarFallbackProps, AvatarImageProps, AvatarRootProps {
alt?: string;
class?: ClassType;
dot?: boolean;
dotClass?: ClassType;
fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
size?: number;
}
defineOptions({
inheritAttrs: false,
});
const props = withDefaults(defineProps<Props>(), {
alt: 'avatar',
as: 'button',
dot: false,
dotClass: 'bg-green-500',
fit: 'cover',
});
const imageStyle = computed<CSSProperties>(() => {
const { fit } = props;
if (fit) {
return { objectFit: fit };
}
return {};
});
const text = computed(() => {
return props.alt.slice(-2).toUpperCase();
});
const rootStyle = computed(() => {
return props.size !== undefined && props.size > 0
? {
height: `${props.size}px`,
width: `${props.size}px`,
}
: {};
});
</script>
<template>
<div
:class="props.class"
:style="rootStyle"
class="relative flex flex-shrink-0 items-center"
>
<Avatar :class="props.class" class="size-full">
<AvatarImage :alt="alt" :src="src" :style="imageStyle" />
<AvatarFallback>{{ text }}</AvatarFallback>
</Avatar>
<span
v-if="dot"
:class="dotClass"
class="absolute bottom-0 right-0 size-3 rounded-full border-2 border-background"
>
</span>
</div>
</template>

View File

@@ -0,0 +1,43 @@
<script lang="ts" setup>
import type { BacktopProps } from './backtop';
import { computed } from 'vue';
import { ArrowUpToLine } from '@vben-core/icons';
import { VbenButton } from '../button';
import { useBackTop } from './use-backtop';
interface Props extends BacktopProps {}
defineOptions({ name: 'BackTop' });
const props = withDefaults(defineProps<Props>(), {
bottom: 20,
isGroup: false,
right: 24,
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="data z-popup fixed bottom-10 size-10 rounded-full bg-background shadow-float duration-500 hover:bg-heavy dark:bg-accent dark:hover:bg-heavy"
size="icon"
variant="icon"
@click="handleClick"
>
<ArrowUpToLine class="size-4" />
</VbenButton>
</transition>
</template>

View File

@@ -0,0 +1,109 @@
<script lang="ts" setup>
import type { BreadcrumbProps } from './types';
import { VbenIcon } from '../icon';
interface Props extends BreadcrumbProps {}
defineOptions({ name: 'Breadcrumb' });
const { breadcrumbs, showIcon } = defineProps<Props>();
const emit = defineEmits<{ select: [string] }>();
function handleClick(index: number, path?: string) {
if (!path || index === breadcrumbs.length - 1) {
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(index, item.path)"
>
<span class="flex-center z-10 h-full">
<VbenIcon
v-if="showIcon"
:icon="item.icon"
class="mr-1 size-4 flex-shrink-0"
/>
<span
:class="{
'font-normal text-foreground':
index === breadcrumbs.length - 1,
}"
>{{ item.title }}
</span>
</span>
</a>
</li>
</template>
</TransitionGroup>
</ul>
</template>
<style scoped>
li {
@apply h-7;
}
li a {
@apply relative mr-9 flex h-7 items-center bg-accent py-0 pl-[5px] pr-2 text-[13px] text-muted-foreground;
}
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 absolute top-0 h-0 w-0 border-[.875rem] border-solid border-accent content-[''];
}
li a::before {
@apply -left-7 z-10 border-l-transparent;
}
li a::after {
@apply left-full border-transparent border-l-accent;
}
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,39 @@
<script lang="ts" setup>
import type { BreadcrumbProps } from './types';
import { useForwardPropsEmits } from 'reka-ui';
import BreadcrumbBackground from './breadcrumb-background.vue';
import Breadcrumb from './breadcrumb.vue';
interface Props extends BreadcrumbProps {
class?: any;
}
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<{ select: [string] }>();
const forward = useForwardPropsEmits(props, emit);
</script>
<template>
<Breadcrumb
v-if="styleType === 'normal'"
v-bind="forward"
class="vben-breadcrumb"
/>
<BreadcrumbBackground
v-if="styleType === 'background'"
v-bind="forward"
class="vben-breadcrumb"
/>
</template>
<style lang="scss" scoped>
/** 修复全局引入Antd时ol和ul的默认样式会被修改的问题 */
.vben-breadcrumb {
:deep(ol),
:deep(ul) {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,53 @@
import type { AsTag } from 'reka-ui';
import type { Component } from 'vue';
import type { ButtonVariants, ButtonVariantSize } from '../../ui';
export interface VbenButtonProps {
/**
* The element or component this component should render as. Can be overwrite by `asChild`
* @defaultValue "div"
*/
as?: AsTag | Component;
/**
* Change the default rendered element for the one passed as a child, merging their props and behavior.
*
* Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details.
*/
asChild?: boolean;
class?: any;
disabled?: boolean;
loading?: boolean;
size?: ButtonVariantSize;
variant?: ButtonVariants;
}
export type CustomRenderType = (() => Component | string) | string;
export type ValueType = boolean | number | string;
export interface VbenButtonGroupProps
extends Pick<VbenButtonProps, 'disabled'> {
/** 单选模式下允许清除选中 */
allowClear?: boolean;
/** 值改变前的回调 */
beforeChange?: (
value: ValueType,
isChecked: boolean,
) => boolean | PromiseLike<boolean | undefined> | undefined;
/** 按钮样式 */
btnClass?: any;
/** 按钮间隔距离 */
gap?: number;
/** 多选模式下限制最多选择的数量。0表示不限制 */
maxCount?: number;
/** 是否允许多选 */
multiple?: boolean;
/** 选项 */
options?: { [key: string]: any; label: CustomRenderType; value: ValueType }[];
/** 显示图标 */
showIcon?: boolean;
/** 尺寸 */
size?: 'large' | 'middle' | 'small';
}

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import type { VbenButtonProps } from './button';
import { computed } from 'vue';
import { LoaderCircle } from '@vben-core/icons';
import { cn } from '@vben-core/shared/utils';
import { Primitive } from 'reka-ui';
import { buttonVariants } from '../../ui';
interface Props extends VbenButtonProps {}
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"
>
<LoaderCircle
v-if="loading"
class="text-md mr-2 size-4 flex-shrink-0 animate-spin"
/>
<slot></slot>
</Primitive>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui';
import { useId } from 'vue';
import { useForwardPropsEmits } from 'reka-ui';
import { Checkbox } from '../../ui/checkbox';
const props = defineProps<CheckboxRootProps & { indeterminate?: boolean }>();
const emits = defineEmits<CheckboxRootEmits>();
const checked = defineModel<boolean>();
const forwarded = useForwardPropsEmits(props, emits);
const id = useId();
</script>
<template>
<div class="flex items-center">
<Checkbox v-bind="forwarded" :id="id" v-model="checked" />
<label :for="id" class="ml-2 cursor-pointer text-sm"> <slot></slot> </label>
</div>
</template>

View File

@@ -0,0 +1,97 @@
<script setup lang="ts">
import type {
ContextMenuContentProps,
ContextMenuRootEmits,
ContextMenuRootProps,
} from 'reka-ui';
import type { ClassType } from '@vben-core/typings';
import type { IContextMenuItem } from './interface';
import { computed } from 'vue';
import { useForwardPropsEmits } from 'reka-ui';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from '../../ui/context-menu';
const props = defineProps<
ContextMenuRootProps & {
class?: ClassType;
contentClass?: ClassType;
contentProps?: ContextMenuContentProps;
handlerData?: Record<string, any>;
itemClass?: ClassType;
menus: (data: any) => IContextMenuItem[];
}
>();
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-popup"
>
<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-2 size-4 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,49 @@
<script lang="ts" setup>
import type {
DropdownMenuProps,
VbenDropdownMenuItem as IDropdownMenuItem,
} from './interface';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../../ui';
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.value">
<DropdownMenuItem
:disabled="menu.disabled"
class="mb-1 cursor-pointer text-foreground/80 data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground"
@click="handleItemClick(menu)"
>
<component :is="menu.icon" v-if="menu.icon" class="mr-2 size-4" />
{{ menu.label }}
</DropdownMenuItem>
<DropdownMenuSeparator v-if="menu.separator" class="bg-border" />
</template>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@@ -0,0 +1,52 @@
<script lang="ts" setup>
import type { DropdownMenuProps } from './interface';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../../ui';
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.value === modelValue
? 'bg-accent text-accent-foreground'
: ''
"
class="mb-1 cursor-pointer text-foreground/80 data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground"
@click="handleItemClick(menu.value)"
>
<component :is="menu.icon" v-if="menu.icon" class="mr-2 size-4" />
<span
v-if="!menu.icon"
:class="menu.value === modelValue ? 'bg-foreground' : ''"
class="mr-2 size-1.5 rounded-full"
></span>
{{ menu.label }}
</DropdownMenuItem>
</template>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import { Maximize, Minimize } from '@vben-core/icons';
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
class="hover:animate-[shrink_0.3s_ease-in-out]"
@click="toggle"
>
<Minimize v-if="isFullscreen" class="size-4 text-foreground" />
<Maximize v-else class="size-4 text-foreground" />
</VbenIconButton>
</template>

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import type {
HoverCardContentProps,
HoverCardRootEmits,
HoverCardRootProps,
} from 'reka-ui';
import type { ClassType } from '@vben-core/typings';
import { computed } from 'vue';
import { useForwardPropsEmits } from 'reka-ui';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '../../ui';
interface Props extends HoverCardRootProps {
class?: ClassType;
contentClass?: ClassType;
contentProps?: HoverCardContentProps;
}
const props = defineProps<Props>();
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-popup"
>
<slot></slot>
</HoverCardContent>
</HoverCard>
</template>

View File

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

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import { ref, useSlots } from 'vue';
import { Eye, EyeOff } from '@vben-core/icons';
import { cn } from '@vben-core/shared/utils';
import { Input } from '../../ui';
import PasswordStrength from './password-strength.vue';
interface Props {
class?: any;
/**
* 是否显示密码强度
*/
passwordStrength?: boolean;
}
defineOptions({
inheritAttrs: false,
});
const props = defineProps<Props>();
const modelValue = defineModel<string>();
const slots = useSlots();
const show = ref(false);
</script>
<template>
<div class="relative w-full">
<Input
v-bind="$attrs"
v-model="modelValue"
:class="cn(props.class)"
:type="show ? 'text' : 'password'"
/>
<template v-if="passwordStrength">
<PasswordStrength :password="modelValue" />
<p v-if="slots.strengthText" class="mt-1.5 text-xs text-muted-foreground">
<slot name="strengthText"> </slot>
</p>
</template>
<div
:class="{
'top-3': !!passwordStrength,
'top-1/2 -translate-y-1/2 items-center': !passwordStrength,
}"
class="absolute inset-y-0 right-0 flex cursor-pointer pr-3 text-lg leading-5 text-foreground/60 hover:text-foreground"
@click="show = !show"
>
<Eye v-if="show" class="size-4" />
<EyeOff v-else class="size-4" />
</div>
</div>
</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="relative mr-1 h-1.5 w-1/5 rounded-sm bg-heavy last:mr-0 dark:bg-input-background"
>
<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,92 @@
<script setup lang="ts">
import { computed } from 'vue';
import { VbenAvatar } from '../avatar';
interface Props {
/**
* @zh_CN 是否收起文本
*/
collapsed?: boolean;
/**
* @zh_CN Logo 图片适应方式
*/
fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/**
* @zh_CN Logo 跳转地址
*/
href?: string;
/**
* @zh_CN Logo 图片大小
*/
logoSize?: number;
/**
* @zh_CN Logo 图标
*/
src?: string;
/**
* @zh_CN 暗色主题 Logo 图标 (可选,若不设置则使用 src)
*/
srcDark?: string;
/**
* @zh_CN Logo 文本
*/
text: string;
/**
* @zh_CN Logo 主题
*/
theme?: string;
}
defineOptions({
name: 'VbenLogo',
});
const props = withDefaults(defineProps<Props>(), {
collapsed: false,
href: 'javascript:void 0',
logoSize: 32,
src: '',
srcDark: '',
theme: 'light',
fit: 'cover',
});
/**
* @zh_CN 根据主题选择合适的 logo 图标
*/
const logoSrc = computed(() => {
// 如果是暗色主题且提供了 srcDark则使用暗色主题的 logo
if (props.theme === 'dark' && props.srcDark) {
return props.srcDark;
}
// 否则使用默认的 src
return props.src;
});
</script>
<template>
<div :class="theme" class="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 text-lg leading-normal transition-all duration-500"
>
<VbenAvatar
v-if="logoSrc"
:alt="text"
:src="logoSrc"
:size="logoSize"
:fit="fit"
class="relative rounded-none bg-transparent"
/>
<template v-if="!collapsed">
<slot name="text">
<span class="truncate text-nowrap font-semibold text-foreground">
{{ text }}
</span>
</slot>
</template>
</a>
</div>
</template>

View File

@@ -0,0 +1,122 @@
<script setup lang="ts">
import type { PinInputProps } from './types';
import { computed, onBeforeUnmount, ref, useId, watch } from 'vue';
import { PinInput, PinInputGroup, PinInputInput } from '../../ui';
import { VbenButton } from '../button';
defineOptions({
inheritAttrs: false,
});
const {
codeLength = 6,
createText = async () => {},
disabled = false,
handleSendCode = async () => {},
loading = false,
maxTime = 60,
} = defineProps<PinInputProps>();
const emit = defineEmits<{
complete: [];
sendError: [error: any];
}>();
const timer = ref<ReturnType<typeof setTimeout>>();
const modelValue = defineModel<string>();
const inputValue = ref<string[]>([]);
const countdown = ref(0);
const btnText = computed(() => {
const countdownValue = countdown.value;
return createText?.(countdownValue);
});
const btnLoading = computed(() => {
return loading || countdown.value > 0;
});
watch(
() => modelValue.value,
() => {
inputValue.value = modelValue.value?.split('') ?? [];
},
);
watch(inputValue, (val) => {
modelValue.value = val.join('');
});
function handleComplete(e: string[]) {
modelValue.value = e.join('');
emit('complete');
}
async function handleSend(e: Event) {
try {
e?.preventDefault();
countdown.value = maxTime;
startCountdown();
await handleSendCode();
} catch (error) {
console.error('Failed to send code:', error);
// Consider emitting an error event or showing a notification
emit('sendError', error);
}
}
function startCountdown() {
if (countdown.value > 0) {
timer.value = setTimeout(() => {
countdown.value--;
startCountdown();
}, 1000);
}
}
onBeforeUnmount(() => {
countdown.value = 0;
clearTimeout(timer.value);
});
const id = useId();
const pinType = 'text' as const;
</script>
<template>
<PinInput
:id="id"
v-model="inputValue"
:disabled="disabled"
class="flex w-full justify-between"
otp
placeholder="○"
:type="pinType"
@complete="handleComplete"
>
<div class="relative flex w-full">
<PinInputGroup class="mr-2">
<PinInputInput
v-for="(item, index) in codeLength"
:key="item"
:index="index"
/>
</PinInputGroup>
<VbenButton
:disabled="disabled"
:loading="btnLoading"
class="flex-grow"
size="lg"
variant="outline"
@click="handleSend"
>
{{ btnText }}
</VbenButton>
</div>
</PinInput>
</template>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import type {
PopoverContentProps,
PopoverRootEmits,
PopoverRootProps,
} from 'reka-ui';
import type { ClassType } from '@vben-core/typings';
import { computed } from 'vue';
import { useForwardPropsEmits } from 'reka-ui';
import {
PopoverContent,
Popover as PopoverRoot,
PopoverTrigger,
} from '../../ui';
interface Props extends PopoverRootProps {
class?: ClassType;
contentClass?: ClassType;
contentProps?: PopoverContentProps;
triggerClass?: ClassType;
}
const props = withDefaults(defineProps<Props>(), {});
const emits = defineEmits<PopoverRootEmits>();
const delegatedProps = computed(() => {
const {
class: _cls,
contentClass: _,
contentProps: _cProps,
triggerClass: _tClass,
...delegated
} = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<PopoverRoot v-bind="forwarded">
<PopoverTrigger :class="triggerClass">
<slot name="trigger"></slot>
<PopoverContent
:class="contentClass"
class="side-content z-popup"
v-bind="contentProps"
>
<slot></slot>
</PopoverContent>
</PopoverTrigger>
</PopoverRoot>
</template>

View File

@@ -0,0 +1,165 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ScrollArea, ScrollBar } from '../../ui';
interface Props {
class?: ClassType;
horizontal?: boolean;
scrollBarClass?: ClassType;
shadow?: boolean;
shadowBorder?: boolean;
shadowBottom?: boolean;
shadowLeft?: boolean;
shadowRight?: boolean;
shadowTop?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
class: '',
horizontal: false,
shadow: false,
shadowBorder: false,
shadowBottom: true,
shadowLeft: false,
shadowRight: false,
shadowTop: true,
});
const emit = defineEmits<{
scrollAt: [{ bottom: boolean; left: boolean; right: boolean; top: boolean }];
}>();
const isAtTop = ref(true);
const isAtRight = ref(false);
const isAtBottom = ref(false);
const isAtLeft = ref(true);
/**
* We have to check if the scroll amount is close enough to some threshold in order to
* more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded
* numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.
* https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
*/
const ARRIVED_STATE_THRESHOLD_PIXELS = 1;
const showShadowTop = computed(() => props.shadow && props.shadowTop);
const showShadowBottom = computed(() => props.shadow && props.shadowBottom);
const showShadowLeft = computed(() => props.shadow && props.shadowLeft);
const showShadowRight = computed(() => props.shadow && props.shadowRight);
const computedShadowClasses = computed(() => {
return {
'both-shadow':
!isAtLeft.value &&
!isAtRight.value &&
showShadowLeft.value &&
showShadowRight.value,
'left-shadow': !isAtLeft.value && showShadowLeft.value,
'right-shadow': !isAtRight.value && showShadowRight.value,
};
});
function handleScroll(event: Event) {
const target = event.target as HTMLElement;
const scrollTop = target?.scrollTop ?? 0;
const scrollLeft = target?.scrollLeft ?? 0;
const clientHeight = target?.clientHeight ?? 0;
const clientWidth = target?.clientWidth ?? 0;
const scrollHeight = target?.scrollHeight ?? 0;
const scrollWidth = target?.scrollWidth ?? 0;
isAtTop.value = scrollTop <= 0;
isAtLeft.value = scrollLeft <= 0;
isAtBottom.value =
Math.abs(scrollTop) + clientHeight >=
scrollHeight - ARRIVED_STATE_THRESHOLD_PIXELS;
isAtRight.value =
Math.abs(scrollLeft) + clientWidth >=
scrollWidth - ARRIVED_STATE_THRESHOLD_PIXELS;
emit('scrollAt', {
bottom: isAtBottom.value,
left: isAtLeft.value,
right: isAtRight.value,
top: isAtTop.value,
});
}
</script>
<template>
<ScrollArea
:class="[cn(props.class), computedShadowClasses]"
:on-scroll="handleScroll"
class="vben-scrollbar relative"
>
<div
v-if="showShadowTop"
:class="{
'opacity-100': !isAtTop,
'border-t border-border': shadowBorder && !isAtTop,
}"
class="scrollbar-top-shadow pointer-events-none absolute top-0 z-10 h-12 w-full opacity-0 transition-opacity duration-300 ease-in-out will-change-[opacity]"
></div>
<slot></slot>
<div
v-if="showShadowBottom"
:class="{
'opacity-100': !isAtTop && !isAtBottom,
'border-b border-border': shadowBorder && !isAtTop && !isAtBottom,
}"
class="scrollbar-bottom-shadow pointer-events-none absolute bottom-0 z-10 h-12 w-full opacity-0 transition-opacity duration-300 ease-in-out will-change-[opacity]"
></div>
<ScrollBar
v-if="horizontal"
:class="scrollBarClass"
orientation="horizontal"
/>
</ScrollArea>
</template>
<style scoped>
.vben-scrollbar {
&:not(.both-shadow).left-shadow {
mask-image: linear-gradient(90deg, transparent, #000 16px);
}
&:not(.both-shadow).right-shadow {
mask-image: linear-gradient(
90deg,
#000 0%,
#000 calc(100% - 16px),
transparent
);
}
&.both-shadow {
mask-image: linear-gradient(
90deg,
transparent,
#000 16px,
#000 calc(100% - 16px),
transparent 100%
);
}
}
.scrollbar-top-shadow {
background: linear-gradient(
to bottom,
hsl(var(--scroll-shadow, var(--background))),
transparent
);
}
.scrollbar-bottom-shadow {
background: linear-gradient(
to top,
hsl(var(--scroll-shadow, var(--background))),
transparent
);
}
</style>

View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
import type { SegmentedItem } from './types';
import { computed } from 'vue';
import { TabsTrigger } from 'reka-ui';
import { Tabs, TabsContent, TabsList } from '../../ui';
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)}%`,
};
});
function activeClass(tab: string): string[] {
return tab === activeTab.value ? ['!font-bold', 'text-primary'] : [];
}
</script>
<template>
<Tabs v-model="activeTab" :default-value="getDefaultValue">
<TabsList
:style="tabsStyle"
class="relative grid w-full bg-accent !outline !outline-2 !outline-heavy"
>
<TabsIndicator :style="tabsIndicatorStyle" />
<template v-for="tab in tabs" :key="tab.value">
<TabsTrigger
:value="tab.value"
:class="activeClass(tab.value)"
class="z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium hover:text-primary 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,37 @@
<script setup lang="ts">
import type { TabsIndicatorProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { TabsIndicator, useForwardProps } from 'reka-ui';
const props = defineProps<TabsIndicatorProps & { class?: any }>();
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-[--reka-tabs-indicator-position] rounded-full px-0 py-1 pr-0.5 transition-[width,transform] duration-300',
props.class,
)
"
>
<div
class="inline-flex h-full w-full items-center justify-center whitespace-nowrap rounded-md bg-background px-3 py-1 text-sm font-medium text-foreground 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,140 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { cn } from '@vben-core/shared/utils';
interface Props {
class?: string;
/**
* @zh_CN 最小加载时间
* @en_US Minimum loading time
*/
minLoadingTime?: number;
/**
* @zh_CN loading状态开启
*/
spinning?: boolean;
/**
* @zh_CN 文字
*/
text?: string;
}
defineOptions({
name: 'VbenLoading',
});
const props = withDefaults(defineProps<Props>(), {
minLoadingTime: 50,
text: '',
});
// const startTime = ref(0);
const showSpinner = ref(false);
const renderSpinner = ref(false);
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="
cn(
'absolute left-0 top-0 z-100 flex size-full flex-col items-center justify-center bg-overlay-content transition-all duration-500 dark:bg-overlay',
{
'invisible opacity-0': !showSpinner,
},
props.class,
)
"
@transitionend="onTransitionEnd"
>
<slot name="icon" v-if="renderSpinner">
<span class="dot relative inline-block size-9 text-3xl">
<i
v-for="index in 4"
:key="index"
class="absolute block size-4 origin-[50%_50%] scale-75 rounded-full bg-primary opacity-30"
></i>
</span>
</slot>
<div v-if="text" class="mt-4 text-xs text-primary">{{ text }}</div>
<slot></slot>
</div>
</template>
<style scoped>
.dot {
transform: rotate(45deg);
animation: rotate-ani 1.2s infinite linear;
}
.dot i {
animation: spin-move-ani 1s infinite linear alternate;
}
.dot i:nth-child(1) {
top: 0;
left: 0;
}
.dot i:nth-child(2) {
top: 0;
right: 0;
animation-delay: 0.4s;
}
.dot i:nth-child(3) {
right: 0;
bottom: 0;
animation-delay: 0.8s;
}
.dot i:nth-child(4) {
bottom: 0;
left: 0;
animation-delay: 1.2s;
}
@keyframes rotate-ani {
to {
transform: rotate(405deg);
}
}
@keyframes spin-move-ani {
to {
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,137 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { cn } from '@vben-core/shared/utils';
interface Props {
class?: string;
/**
* @zh_CN 最小加载时间
* @en_US Minimum loading time
*/
minLoadingTime?: number;
/**
* @zh_CN loading状态开启
*/
spinning?: boolean;
}
defineOptions({
name: 'VbenSpinner',
});
const props = withDefaults(defineProps<Props>(), {
minLoadingTime: 50,
});
// const startTime = ref(0);
const showSpinner = ref(false);
const renderSpinner = ref(false);
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="
cn(
'flex-center absolute left-0 top-0 z-100 size-full bg-overlay-content backdrop-blur-sm transition-all duration-500',
{
'invisible opacity-0': !showSpinner,
},
props.class,
)
"
@transitionend="onTransitionEnd"
>
<div
:class="{ paused: !renderSpinner }"
v-if="renderSpinner"
class="loader relative size-12 before:absolute before:left-0 before:top-[60px] before:h-[5px] before:w-12 before:rounded-[50%] before:bg-primary/50 before:content-[''] after:absolute after:left-0 after:top-0 after:h-full after:w-full after:rounded after:bg-primary after:content-['']"
></div>
</div>
</template>
<style scoped>
.paused {
&::before {
animation-play-state: paused !important;
}
&::after {
animation-play-state: paused !important;
}
}
.loader {
&::before {
animation: loader-shadow-ani 0.5s linear infinite;
}
&::after {
animation: loader-jump-ani 0.5s linear infinite;
}
}
@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,31 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
import { CircleHelp } from 'lucide-vue-next';
import Tooltip from './tooltip.vue';
defineOptions({
inheritAttrs: false,
});
defineProps<{ triggerClass?: string }>();
</script>
<template>
<Tooltip :delay-duration="300" side="right">
<template #trigger>
<slot name="trigger">
<CircleHelp
:class="
cn(
'inline-flex size-5 cursor-pointer text-foreground/80 hover:text-foreground',
triggerClass,
)
"
/>
</slot>
</template>
<slot></slot>
</Tooltip>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import type { TooltipContentProps } from 'reka-ui';
import type { StyleValue } from 'vue';
import type { ClassType } from '@vben-core/typings';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '../../ui';
interface Props {
contentClass?: ClassType;
contentStyle?: StyleValue;
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
:class="contentClass"
:side="side"
:style="contentStyle"
class="side-content rounded-md bg-accent text-popover-foreground"
>
<slot></slot>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>

View File

@@ -0,0 +1,3 @@
export * from './components';
export * from './ui';
export { createContext, Slot, VisuallyHidden } from 'reka-ui';

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { AccordionRootEmits, AccordionRootProps } from 'reka-ui';
import { AccordionRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<AccordionRootProps>();
const emits = defineEmits<AccordionRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot></slot>
</AccordionRoot>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { AccordionContentProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { AccordionContent } from 'reka-ui';
const props = defineProps<AccordionContentProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AccordionContent
v-bind="delegatedProps"
class="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
>
<div :class="cn('pb-4 pt-0', props.class)">
<slot></slot>
</div>
</AccordionContent>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import type { AccordionItemProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { AccordionItem, useForwardProps } from 'reka-ui';
const props = defineProps<AccordionItemProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
<slot></slot>
</AccordionItem>
</template>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import type { AccordionTriggerProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ChevronDown } from 'lucide-vue-next';
import { AccordionHeader, AccordionTrigger } from 'reka-ui';
const props = defineProps<AccordionTriggerProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
>
<slot></slot>
<slot name="icon">
<ChevronDown
class="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200"
/>
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { AlertDialogEmits, AlertDialogProps } from 'reka-ui';
import { AlertDialogRoot, useForwardPropsEmits } from 'reka-ui';
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,13 @@
<script setup lang="ts">
import type { AlertDialogActionProps } from 'reka-ui';
import { AlertDialogAction } from 'reka-ui';
const props = defineProps<AlertDialogActionProps>();
</script>
<template>
<AlertDialogAction v-bind="props">
<slot></slot>
</AlertDialogAction>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { AlertDialogCancelProps } from 'reka-ui';
import { AlertDialogCancel } from 'reka-ui';
const props = defineProps<AlertDialogCancelProps>();
</script>
<template>
<AlertDialogCancel v-bind="props">
<slot></slot>
</AlertDialogCancel>
</template>

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { AlertDialogContentEmits, AlertDialogContentProps } from 'reka-ui';
import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import {
AlertDialogContent,
AlertDialogPortal,
useForwardPropsEmits,
} from 'reka-ui';
import AlertDialogOverlay from './AlertDialogOverlay.vue';
const props = withDefaults(
defineProps<
AlertDialogContentProps & {
centered?: boolean;
class?: ClassType;
modal?: boolean;
open?: boolean;
overlayBlur?: number;
zIndex?: number;
}
>(),
{ modal: true },
);
const emits = defineEmits<
AlertDialogContentEmits & { close: []; closed: []; opened: [] }
>();
const delegatedProps = computed(() => {
const { class: _, modal: _modal, open: _open, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof AlertDialogContent> | null>(null);
function onAnimationEnd(event: AnimationEvent) {
// 只有在 contentRef 的动画结束时才触发 opened/closed 事件
if (event.target === contentRef.value?.$el) {
if (props.open) {
emits('opened');
} else {
emits('closed');
}
}
}
defineExpose({
getContentRef: () => contentRef.value,
});
</script>
<template>
<AlertDialogPortal>
<Transition name="fade" appear>
<AlertDialogOverlay
v-if="open && modal"
:style="{
...(zIndex ? { zIndex } : {}),
position: 'fixed',
backdropFilter:
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
}"
@click="() => emits('close')"
/>
</Transition>
<AlertDialogContent
ref="contentRef"
:style="{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }"
@animationend="onAnimationEnd"
v-bind="forwarded"
:class="
cn(
'z-popup bg-background p-6 shadow-lg outline-none sm:rounded-xl',
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
{
'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':
!centered,
'data-[state=closed]:slide-out-to-top-[148%] data-[state=open]:slide-in-from-top-[98%]':
centered,
'top-[10vh]': !centered,
'top-1/2 -translate-y-1/2': centered,
},
props.class,
)
"
>
<slot></slot>
</AlertDialogContent>
</AlertDialogPortal>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { AlertDialogDescriptionProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { AlertDialogDescription, useForwardProps } from 'reka-ui';
const props = defineProps<AlertDialogDescriptionProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AlertDialogDescription
v-bind="forwardedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot></slot>
</AlertDialogDescription>
</template>

View File

@@ -0,0 +1,8 @@
<script setup lang="ts">
import { useScrollLock } from '@vben-core/composables';
useScrollLock();
</script>
<template>
<div class="z-popup inset-0 bg-overlay"></div>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { AlertDialogTitleProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { AlertDialogTitle, useForwardProps } from 'reka-ui';
const props = defineProps<AlertDialogTitleProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AlertDialogTitle
v-bind="forwardedProps"
:class="
cn('text-lg font-semibold leading-none tracking-tight', props.class)
"
>
<slot></slot>
</AlertDialogTitle>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { AvatarVariants } from './avatar';
import { cn } from '@vben-core/shared/utils';
import { AvatarRoot } from 'reka-ui';
import { avatarVariant } from './avatar';
const props = withDefaults(
defineProps<{
class?: any;
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,13 @@
<script setup lang="ts">
import type { AvatarFallbackProps } from 'reka-ui';
import { AvatarFallback } from 'reka-ui';
const props = defineProps<AvatarFallbackProps>();
</script>
<template>
<AvatarFallback v-bind="props">
<slot></slot>
</AvatarFallback>
</template>

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { PrimitiveProps } from 'reka-ui';
import { cn } from '@vben-core/shared/utils';
import { Primitive } from 'reka-ui';
const props = withDefaults(defineProps<PrimitiveProps & { class?: any }>(), {
as: 'a',
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('transition-colors hover:text-foreground', props.class)"
>
<slot></slot>
</Primitive>
</template>

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui';
import type { ButtonVariants, ButtonVariantSize } from './types';
import { cn } from '@vben-core/shared/utils';
import { Primitive } from 'reka-ui';
import { buttonVariants } from './button';
interface Props extends PrimitiveProps {
class?: any;
size?: ButtonVariantSize;
variant?: ButtonVariants;
}
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,20 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<div
:class="
cn(
'rounded-xl border border-border bg-card text-card-foreground',
props.class,
)
"
>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<p :class="cn('text-sm text-muted-foreground', props.class)">
<slot></slot>
</p>
</template>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Check, Minus } from 'lucide-vue-next';
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<
CheckboxRootProps & { class?: any; indeterminate?: boolean }
>();
const emits = defineEmits<CheckboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<CheckboxRoot
v-bind="forwarded"
:class="
cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-border transition hover:border-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class,
)
"
>
<CheckboxIndicator
class="flex h-full w-full items-center justify-center text-current"
>
<slot>
<component :is="indeterminate ? Minus : Check" class="h-4 w-4" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { ContextMenuRootEmits, ContextMenuRootProps } from 'reka-ui';
import { ContextMenuRoot, useForwardPropsEmits } from 'reka-ui';
const props = withDefaults(defineProps<ContextMenuRootProps>(), {
modal: false,
});
const emits = defineEmits<ContextMenuRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<ContextMenuRoot v-bind="forwarded">
<slot></slot>
</ContextMenuRoot>
</template>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import type {
ContextMenuCheckboxItemEmits,
ContextMenuCheckboxItemProps,
} from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Check } from 'lucide-vue-next';
import {
ContextMenuCheckboxItem,
ContextMenuItemIndicator,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<ContextMenuCheckboxItemProps & { class?: any }>();
const emits = defineEmits<ContextMenuCheckboxItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ContextMenuCheckboxItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)
"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuItemIndicator>
<Check class="h-4 w-4" />
</ContextMenuItemIndicator>
</span>
<slot></slot>
</ContextMenuCheckboxItem>
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import type { ContextMenuContentEmits, ContextMenuContentProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import {
ContextMenuContent,
ContextMenuPortal,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<ContextMenuContentProps & { class?: any }>();
const emits = defineEmits<ContextMenuContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ContextMenuPortal>
<ContextMenuContent
v-bind="forwarded"
:class="
cn(
'z-popup min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot></slot>
</ContextMenuContent>
</ContextMenuPortal>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { ContextMenuGroupProps } from 'reka-ui';
import { ContextMenuGroup } from 'reka-ui';
const props = defineProps<ContextMenuGroupProps>();
</script>
<template>
<ContextMenuGroup v-bind="props">
<slot></slot>
</ContextMenuGroup>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import type { ContextMenuItemEmits, ContextMenuItemProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ContextMenuItem, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<
ContextMenuItemProps & { class?: any; inset?: boolean }
>();
const emits = defineEmits<ContextMenuItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ContextMenuItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
props.class,
)
"
>
<slot></slot>
</ContextMenuItem>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import type { ContextMenuLabelProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ContextMenuLabel } from 'reka-ui';
const props = defineProps<
ContextMenuLabelProps & { class?: any; inset?: boolean }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<ContextMenuLabel
v-bind="delegatedProps"
:class="
cn(
'px-2 py-1.5 text-sm font-semibold text-foreground',
inset && 'pl-8',
props.class,
)
"
>
<slot></slot>
</ContextMenuLabel>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { ContextMenuPortalProps } from 'reka-ui';
import { ContextMenuPortal } from 'reka-ui';
const props = defineProps<ContextMenuPortalProps>();
</script>
<template>
<ContextMenuPortal v-bind="props">
<slot></slot>
</ContextMenuPortal>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type {
ContextMenuRadioGroupEmits,
ContextMenuRadioGroupProps,
} from 'reka-ui';
import { ContextMenuRadioGroup, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<ContextMenuRadioGroupProps>();
const emits = defineEmits<ContextMenuRadioGroupEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<ContextMenuRadioGroup v-bind="forwarded">
<slot></slot>
</ContextMenuRadioGroup>
</template>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import type {
ContextMenuRadioItemEmits,
ContextMenuRadioItemProps,
} from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Circle } from 'lucide-vue-next';
import {
ContextMenuItemIndicator,
ContextMenuRadioItem,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<ContextMenuRadioItemProps & { class?: any }>();
const emits = defineEmits<ContextMenuRadioItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ContextMenuRadioItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)
"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuItemIndicator>
<Circle class="h-2 w-2 fill-current" />
</ContextMenuItemIndicator>
</span>
<slot></slot>
</ContextMenuRadioItem>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { ContextMenuSeparatorProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ContextMenuSeparator } from 'reka-ui';
const props = defineProps<ContextMenuSeparatorProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<ContextMenuSeparator
v-bind="delegatedProps"
:class="cn('-mx-1 my-1 h-px bg-border', props.class)"
/>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<span
:class="
cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)
"
>
<slot></slot>
</span>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { ContextMenuSubEmits, ContextMenuSubProps } from 'reka-ui';
import { ContextMenuSub, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<ContextMenuSubProps>();
const emits = defineEmits<ContextMenuSubEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<ContextMenuSub v-bind="forwarded">
<slot></slot>
</ContextMenuSub>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import type {
DropdownMenuSubContentEmits,
DropdownMenuSubContentProps,
} from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ContextMenuSubContent, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<DropdownMenuSubContentProps & { class?: any }>();
const emits = defineEmits<DropdownMenuSubContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ContextMenuSubContent
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md',
'border border-border',
'bg-popover p-1 text-popover-foreground shadow-lg',
'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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot></slot>
</ContextMenuSubContent>
</template>

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { ContextMenuSubTriggerProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ChevronRight } from 'lucide-vue-next';
import { ContextMenuSubTrigger, useForwardProps } from 'reka-ui';
const props = defineProps<
ContextMenuSubTriggerProps & {
class?: any;
inset?: boolean;
}
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<ContextMenuSubTrigger
v-bind="forwardedProps"
:class="
cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
inset && 'pl-8',
props.class,
)
"
>
<slot></slot>
<ChevronRight class="ml-auto h-4 w-4" />
</ContextMenuSubTrigger>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { ContextMenuTriggerProps } from 'reka-ui';
import { ContextMenuTrigger, useForwardProps } from 'reka-ui';
const props = defineProps<ContextMenuTriggerProps>();
const forwardedProps = useForwardProps(props);
</script>
<template>
<ContextMenuTrigger v-bind="forwardedProps">
<slot></slot>
</ContextMenuTrigger>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from 'reka-ui';
import { DialogRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot></slot>
</DialogRoot>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { DialogCloseProps } from 'reka-ui';
import { DialogClose } from 'reka-ui';
const props = defineProps<DialogCloseProps>();
</script>
<template>
<DialogClose v-bind="props">
<slot></slot>
</DialogClose>
</template>

View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { X } from 'lucide-vue-next';
import { DialogClose, DialogContent, useForwardPropsEmits } from 'reka-ui';
import DialogOverlay from './DialogOverlay.vue';
const props = withDefaults(
defineProps<
DialogContentProps & {
animationType?: 'scale' | 'slide';
appendTo?: HTMLElement | string;
class?: ClassType;
closeClass?: ClassType;
closeDisabled?: boolean;
modal?: boolean;
open?: boolean;
overlayBlur?: number;
showClose?: boolean;
zIndex?: number;
}
>(),
{
appendTo: 'body',
animationType: 'slide',
closeDisabled: false,
showClose: true,
},
);
const emits = defineEmits<
DialogContentEmits & { close: []; closed: []; opened: [] }
>();
const delegatedProps = computed(() => {
const {
class: _,
modal: _modal,
open: _open,
showClose: __,
animationType: ___,
...delegated
} = props;
return delegated;
});
function isAppendToBody() {
return (
props.appendTo === 'body' ||
props.appendTo === document.body ||
!props.appendTo
);
}
const position = computed(() => {
return isAppendToBody() ? 'fixed' : 'absolute';
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
function onAnimationEnd(event: AnimationEvent) {
// 只有在 contentRef 的动画结束时才触发 opened/closed 事件
if (event.target === contentRef.value?.$el) {
if (props.open) {
emits('opened');
} else {
emits('closed');
}
}
}
defineExpose({
getContentRef: () => contentRef.value,
});
</script>
<template>
<Teleport defer :to="appendTo">
<Transition name="fade">
<DialogOverlay
v-if="open && modal"
:style="{
...(zIndex ? { zIndex } : {}),
position,
backdropFilter:
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
}"
@click="() => emits('close')"
/>
</Transition>
<DialogContent
ref="contentRef"
:style="{ ...(zIndex ? { zIndex } : {}), position }"
@animationend="onAnimationEnd"
v-bind="forwarded"
:class="
cn(
'z-popup w-full bg-background p-6 shadow-lg outline-none 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 sm:rounded-xl',
{
'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':
animationType === 'slide',
},
props.class,
)
"
>
<slot></slot>
<DialogClose
v-if="showClose"
:disabled="closeDisabled"
:class="
cn(
'flex-center absolute right-3 top-3 h-6 w-6 rounded-full px-1 text-lg text-foreground/80 opacity-70 transition-opacity hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground',
props.closeClass,
)
"
@click="() => emits('close')"
>
<X class="h-4 w-4" />
</DialogClose>
</DialogContent>
</Teleport>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { DialogDescription, useForwardProps } from 'reka-ui';
const props = defineProps<DialogDescriptionProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DialogDescription
v-bind="forwardedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot></slot>
</DialogDescription>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { inject } from 'vue';
import { useScrollLock } from '@vben-core/composables';
useScrollLock();
const id = inject('DISMISSABLE_MODAL_ID');
</script>
<template>
<div :data-dismissable-modal="id" class="z-popup inset-0 bg-overlay"></div>
</template>

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { X } from 'lucide-vue-next';
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from 'reka-ui';
const props = withDefaults(
defineProps<DialogContentProps & { class?: any; zIndex?: number }>(),
{ zIndex: 1000 },
);
const emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DialogPortal>
<DialogOverlay
:style="{ zIndex }"
class="absolute inset-0 grid place-items-center overflow-y-auto border border-border bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 my-8 grid w-full max-w-lg gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class,
)
"
:style="{ zIndex }"
v-bind="forwarded"
@pointer-down-outside="
(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (
originalEvent.offsetX > target.clientWidth ||
originalEvent.offsetY > target.clientHeight
) {
event.preventDefault();
}
}
"
>
<slot></slot>
<DialogClose
class="absolute right-4 top-4 rounded-md p-0.5 transition-colors hover:bg-secondary"
>
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { DialogTitleProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { DialogTitle, useForwardProps } from 'reka-ui';
const props = defineProps<DialogTitleProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DialogTitle
v-bind="forwardedProps"
:class="
cn('text-lg font-semibold leading-none tracking-tight', props.class)
"
>
<slot></slot>
</DialogTitle>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { DialogTriggerProps } from 'reka-ui';
import { DialogTrigger } from 'reka-ui';
const props = defineProps<DialogTriggerProps>();
</script>
<template>
<DialogTrigger v-bind="props">
<slot></slot>
</DialogTrigger>
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from 'reka-ui';
import { DropdownMenuRoot, useForwardPropsEmits } from 'reka-ui';
const props = withDefaults(defineProps<DropdownMenuRootProps>(), {
modal: false,
});
const emits = defineEmits<DropdownMenuRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRoot v-bind="forwarded">
<slot></slot>
</DropdownMenuRoot>
</template>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import type {
DropdownMenuCheckboxItemEmits,
DropdownMenuCheckboxItemProps,
} from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Check } from 'lucide-vue-next';
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: any }>();
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuCheckboxItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)
"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Check class="h-4 w-4" />
</DropdownMenuItemIndicator>
</span>
<slot></slot>
</DropdownMenuCheckboxItem>
</template>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import type {
DropdownMenuContentEmits,
DropdownMenuContentProps,
} from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from 'reka-ui';
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: any }>(),
{
sideOffset: 4,
},
);
const emits = defineEmits<DropdownMenuContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
v-bind="forwarded"
:class="
cn(
'z-popup min-w-32 overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot></slot>
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { DropdownMenuGroupProps } from 'reka-ui';
import { DropdownMenuGroup } from 'reka-ui';
const props = defineProps<DropdownMenuGroupProps>();
</script>
<template>
<DropdownMenuGroup v-bind="props">
<slot></slot>
</DropdownMenuGroup>
</template>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { DropdownMenuItemProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { DropdownMenuItem, useForwardProps } from 'reka-ui';
const props = defineProps<
DropdownMenuItemProps & { class?: any; inset?: boolean }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuItem
v-bind="forwardedProps"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
props.class,
)
"
>
<slot></slot>
</DropdownMenuItem>
</template>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { DropdownMenuLabelProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { DropdownMenuLabel, useForwardProps } from 'reka-ui';
const props = defineProps<
DropdownMenuLabelProps & { class?: any; inset?: boolean }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuLabel
v-bind="forwardedProps"
:class="
cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)
"
>
<slot></slot>
</DropdownMenuLabel>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type {
DropdownMenuRadioGroupEmits,
DropdownMenuRadioGroupProps,
} from 'reka-ui';
import { DropdownMenuRadioGroup, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<DropdownMenuRadioGroupProps>();
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRadioGroup v-bind="forwarded">
<slot></slot>
</DropdownMenuRadioGroup>
</template>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import type {
DropdownMenuRadioItemEmits,
DropdownMenuRadioItemProps,
} from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Circle } from 'lucide-vue-next';
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<DropdownMenuRadioItemProps & { class?: any }>();
const emits = defineEmits<DropdownMenuRadioItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuRadioItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)
"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuItemIndicator>
</span>
<slot></slot>
</DropdownMenuRadioItem>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { DropdownMenuSeparatorProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { DropdownMenuSeparator } from 'reka-ui';
const props = defineProps<
DropdownMenuSeparatorProps & {
class?: any;
}
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<DropdownMenuSeparator
v-bind="delegatedProps"
:class="cn('-mx-1 my-1 h-px bg-border', props.class)"
/>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from 'reka-ui';
import { DropdownMenuSub, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<DropdownMenuSubProps>();
const emits = defineEmits<DropdownMenuSubEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuSub v-bind="forwarded">
<slot></slot>
</DropdownMenuSub>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import type {
DropdownMenuSubContentEmits,
DropdownMenuSubContentProps,
} from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { DropdownMenuSubContent, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<DropdownMenuSubContentProps & { class?: any }>();
const emits = defineEmits<DropdownMenuSubContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuSubContent
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md',
'border border-border',
'bg-popover p-1 text-popover-foreground shadow-lg',
'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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot></slot>
</DropdownMenuSubContent>
</template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import type { DropdownMenuSubTriggerProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ChevronRight } from 'lucide-vue-next';
import { DropdownMenuSubTrigger, useForwardProps } from 'reka-ui';
const props = defineProps<DropdownMenuSubTriggerProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuSubTrigger
v-bind="forwardedProps"
:class="
cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
props.class,
)
"
>
<slot></slot>
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuSubTrigger>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DropdownMenuTriggerProps } from 'reka-ui';
import { DropdownMenuTrigger, useForwardProps } from 'reka-ui';
const props = defineProps<DropdownMenuTriggerProps>();
const forwardedProps = useForwardProps(props);
</script>
<template>
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
<slot></slot>
</DropdownMenuTrigger>
</template>

View File

@@ -0,0 +1,16 @@
export { default as DropdownMenu } from './DropdownMenu.vue';
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue';
export { default as DropdownMenuContent } from './DropdownMenuContent.vue';
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue';
export { default as DropdownMenuItem } from './DropdownMenuItem.vue';
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue';
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue';
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue';
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue';
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue';
export { default as DropdownMenuSub } from './DropdownMenuSub.vue';
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue';
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue';
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue';
export { DropdownMenuPortal } from 'reka-ui';

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
import { Slot } from 'reka-ui';
import { useFormField } from './useFormField';
const { error, formDescriptionId, formItemId, formMessageId } = useFormField();
</script>
<template>
<Slot
:id="formItemId"
:aria-describedby="
!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
"
:aria-invalid="!!error"
>
<slot></slot>
</Slot>
</template>

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import { cn } from '@vben-core/shared/utils';
import { useFormField } from './useFormField';
const props = defineProps<{
class?: any;
}>();
const { formDescriptionId } = useFormField();
</script>
<template>
<p
:id="formDescriptionId"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot></slot>
</p>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import type { LabelProps } from 'reka-ui';
import { cn } from '@vben-core/shared/utils';
import { Label } from '../label';
import { useFormField } from './useFormField';
const props = defineProps<LabelProps & { class?: any }>();
const { formItemId } = useFormField();
</script>
<template>
<Label :class="cn(props.class)" :for="formItemId">
<slot></slot>
</Label>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import { toValue } from 'vue';
import { ErrorMessage } from 'vee-validate';
import { useFormField } from './useFormField';
const { formMessageId, name } = useFormField();
</script>
<template>
<ErrorMessage
:id="formMessageId"
:name="toValue(name)"
as="p"
class="text-[0.8rem] text-destructive"
/>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HoverCardRootEmits, HoverCardRootProps } from 'reka-ui';
import { HoverCardRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<HoverCardRootProps>();
const emits = defineEmits<HoverCardRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<HoverCardRoot v-bind="forwarded">
<slot></slot>
</HoverCardRoot>
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import type { HoverCardContentProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { HoverCardContent, HoverCardPortal, useForwardProps } from 'reka-ui';
const props = withDefaults(
defineProps<HoverCardContentProps & { class?: any }>(),
{
sideOffset: 4,
},
);
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<HoverCardPortal>
<HoverCardContent
v-bind="forwardedProps"
:class="
cn(
'z-popup w-64 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot></slot>
</HoverCardContent>
</HoverCardPortal>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { HoverCardTriggerProps } from 'reka-ui';
import { HoverCardTrigger } from 'reka-ui';
const props = defineProps<HoverCardTriggerProps>();
</script>
<template>
<HoverCardTrigger v-bind="props">
<slot></slot>
</HoverCardTrigger>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
import { useVModel } from '@vueuse/core';
const props = defineProps<{
class?: any;
defaultValue?: number | string;
modelValue?: number | string;
}>();
const emits = defineEmits<{
(e: 'update:modelValue', payload: number | string): void;
}>();
const modelValue = useVModel(props, 'modelValue', emits, {
defaultValue: props.defaultValue,
passive: true,
});
</script>
<template>
<input
v-model="modelValue"
:class="
cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"
/>
</template>
<style lang="scss" scoped>
input {
--ring: var(--primary);
}
</style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { LabelProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Label } from 'reka-ui';
const props = defineProps<LabelProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<Label
v-bind="delegatedProps"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class,
)
"
>
<slot></slot>
</Label>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { NumberFieldRootEmits, NumberFieldRootProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { NumberFieldRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<NumberFieldRootProps & { class?: any }>();
const emits = defineEmits<NumberFieldRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<NumberFieldRoot v-bind="forwarded" :class="cn('grid gap-1.5', props.class)">
<slot></slot>
</NumberFieldRoot>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import type { NumberFieldDecrementProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Minus } from 'lucide-vue-next';
import { NumberFieldDecrement, useForwardProps } from 'reka-ui';
const props = defineProps<NumberFieldDecrementProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<NumberFieldDecrement
data-slot="decrement"
v-bind="forwarded"
:class="
cn(
'absolute left-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20',
props.class,
)
"
>
<slot>
<Minus class="h-4 w-4" />
</slot>
</NumberFieldDecrement>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import type { NumberFieldIncrementProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Plus } from 'lucide-vue-next';
import { NumberFieldIncrement, useForwardProps } from 'reka-ui';
const props = defineProps<NumberFieldIncrementProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<NumberFieldIncrement
data-slot="increment"
v-bind="forwarded"
:class="
cn(
'absolute right-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20',
props.class,
)
"
>
<slot>
<Plus class="h-4 w-4" />
</slot>
</NumberFieldIncrement>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
import { NumberFieldInput } from 'reka-ui';
</script>
<template>
<NumberFieldInput
:class="
cn(
'flex h-9 w-full rounded-md border border-input bg-transparent py-1 text-center text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
)
"
data-slot="input"
/>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { PaginationEllipsisProps } from 'reka-ui';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { MoreHorizontal } from 'lucide-vue-next';
import { PaginationEllipsis } from 'reka-ui';
const props = defineProps<PaginationEllipsisProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<PaginationEllipsis
v-bind="delegatedProps"
:class="cn('flex size-8 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="size-4" />
</slot>
</PaginationEllipsis>
</template>

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