chore: 升级 shadcn-vue 组件到v4最新版 (#7972)

* fix: useStore is deprecated

* chore: update deps

* feat: 升级shadcn-ui v4

* fix: workbench style

* feat: 升级shadcn-ui v4 step2

* feat: 升级shadcn-ui v4 step3

* chore: 升级shadcn v4

* fix: pagination

* fix: dark style

* fix: doc import

* feat: 增加详情组件,参考 antdv-next

* docs: descriptions docs

* docs: Browser Support

* feat: add table action

* feat: icon use vbenIcon

* fix: type error

* fix: dropdown popConfirm

* feat: 使用默认的文字交互

* feat: 优化渲染性能
This commit is contained in:
xingyu
2026-05-31 15:18:46 +08:00
committed by GitHub
parent 108d7ff335
commit 04fbb7a556
239 changed files with 6015 additions and 3205 deletions

View File

@@ -36,7 +36,7 @@
},
"dependencies": {
"@iconify/vue": "catalog:",
"lucide-vue-next": "catalog:",
"@lucide/vue": "catalog:",
"vue": "catalog:"
}
}

View File

@@ -35,7 +35,6 @@ export {
EyeOff,
FoldHorizontal,
Fullscreen,
Github,
Grid,
Grip,
GripVertical,
@@ -93,4 +92,4 @@ export {
Unlink2,
UserRoundPen,
X,
} from 'lucide-vue-next';
} from '@lucide/vue';

View File

@@ -9,6 +9,7 @@ export const messages: Record<Locale, Record<string, string>> = {
prompt: 'Prompt',
reset: 'Reset',
submit: 'Submit',
confirmTitle: 'Please Confirm',
},
'zh-CN': {
cancel: '取消',
@@ -18,6 +19,7 @@ export const messages: Record<Locale, Record<string, string>> = {
prompt: '提示',
reset: '重置',
submit: '提交',
confirmTitle: '请确认',
},
};

View File

@@ -6,7 +6,7 @@ import type {
import { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue';
import { useStore } from '@vben-core/shared/store';
import { useSelector } from '@vben-core/shared/store';
import { FormApi } from './form-api';
import VbenUseForm from './vben-use-form.vue';
@@ -19,7 +19,7 @@ export function useVbenForm<
const api = new FormApi(options as unknown as VbenFormProps);
const extendedApi: ExtendedFormApi = api as never;
extendedApi.useStore = (selector) => {
return useStore(api.store, selector);
return useSelector(api.store, selector);
};
const Form = defineComponent(

View File

@@ -54,7 +54,8 @@ const components = globalShareState.getComponents();
const id = useId();
provide('DISMISSABLE_DRAWER_ID', id);
// const wrapperRef = ref<HTMLElement>();
// @ts-expect-error unused
const wrapperRef = ref<HTMLElement>();
const { $t } = useSimpleLocale();
const { isMobile } = useIsMobile();
@@ -285,8 +286,8 @@ const getForceMount = computed(() => {
<SheetDescription />
</VisuallyHidden>
</template>
<!-- 注释掉的部分 <div ref="wrapperRef" -->
<div
ref="wrapperRef"
:class="
cn('relative flex-1 overflow-y-auto p-3', contentClass, {
'pointer-events-none': showLoading || submitting,

View File

@@ -15,7 +15,7 @@ import {
} from 'vue';
import { usePreferences } from '@vben-core/preferences';
import { useStore } from '@vben-core/shared/store';
import { useSelector } from '@vben-core/shared/store';
import { DrawerApi } from './drawer-api';
import VbenDrawer from './drawer.vue';
@@ -109,7 +109,7 @@ export function useVbenDrawer<
const extendedApi: ExtendedDrawerApi = api as never;
extendedApi.useStore = (selector) => {
return useStore(api.store, selector);
return useSelector(api.store, selector);
};
const Drawer = defineComponent(

View File

@@ -11,7 +11,7 @@ import {
} from 'vue';
import { usePreferences } from '@vben-core/preferences';
import { useStore } from '@vben-core/shared/store';
import { useSelector } from '@vben-core/shared/store';
import { ModalApi } from './modal-api';
import VbenModal from './modal.vue';
@@ -116,7 +116,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
const extendedApi: ExtendedModalApi = api as never;
extendedApi.useStore = (selector) => {
return useStore(api.store, selector);
return useSelector(api.store, selector);
};
const Modal = defineComponent(

View File

@@ -37,6 +37,7 @@
}
},
"dependencies": {
"@lucide/vue": "catalog:",
"@vben-core/composables": "workspace:*",
"@vben-core/design": "workspace:*",
"@vben-core/icons": "workspace:*",
@@ -44,7 +45,6 @@
"@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

@@ -34,7 +34,7 @@ const { handleClick, visible } = useBackTop(props);
:style="backTopStyle"
class="data z-popup bg-background shadow-float hover:bg-heavy dark:bg-accent dark:hover:bg-heavy fixed bottom-10 size-10 rounded-full duration-500"
size="icon"
variant="icon"
variant="ghost"
@click="handleClick"
>
<ArrowUpToLine class="size-4" />

View File

@@ -2,7 +2,7 @@ import type { AsTag } from 'reka-ui';
import type { Component } from 'vue';
import type { ButtonVariants, ButtonVariantSize } from '../../ui';
import type { ButtonVariants } from '../../ui';
export interface VbenButtonProps {
/**
@@ -19,8 +19,8 @@ export interface VbenButtonProps {
class?: any;
disabled?: boolean;
loading?: boolean;
size?: ButtonVariantSize;
variant?: ButtonVariants;
size?: ButtonVariants['size'];
variant?: ButtonVariants['variant'];
}
export type CustomRenderType = (() => Component | string) | string;

View File

@@ -16,7 +16,7 @@ interface Props extends VbenButtonProps {
tooltip?: string;
tooltipDelayDuration?: number;
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
variant?: ButtonVariants;
variant?: ButtonVariants['variant'];
}
const props = withDefaults(defineProps<Props>(), {
@@ -24,7 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
onClick: () => {},
tooltipDelayDuration: 200,
tooltipSide: 'bottom',
variant: 'icon',
variant: 'ghost',
});
const slots = useSlots();

View File

@@ -7,7 +7,7 @@ import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
import { useNamespace } from '@vben-core/composables';
import { ChevronsDown } from 'lucide-vue-next';
import { ChevronsDown } from '@lucide/vue';
import {
CollapsibleContent,
CollapsibleRoot,

View File

@@ -5,7 +5,7 @@ import type { ClassType } from '@vben-core/typings';
import { computed } from 'vue';
import { ChevronsDown } from 'lucide-vue-next';
import { ChevronsDown } from '@lucide/vue';
import {
CollapsibleContent,
CollapsibleRoot,

View File

@@ -0,0 +1,129 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import type { DescriptionsRenderNode, DescriptionsSize } from './types';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { VbenRenderContent } from '../render-content';
interface Props {
/** 是否边框模式 */
bordered?: boolean;
/** 是否显示冒号(仅非边框模式生效) */
colon?: boolean;
/** 内容 */
content?: DescriptionsRenderNode | null;
/** 内容样式 */
contentStyle?: CSSProperties;
/** 单项自定义类名 */
itemClass?: string;
/** 标签 */
label?: DescriptionsRenderNode | null;
/** 标签样式 */
labelStyle?: CSSProperties;
/** 尺寸 */
size?: DescriptionsSize;
/** 跨列数 */
span?: number;
/** 渲染标签 th 还是 td */
tag: 'td' | 'th';
/** 单元格类型 */
type: 'content' | 'item' | 'label';
}
const props = withDefaults(defineProps<Props>(), {
bordered: false,
colon: true,
content: null,
contentStyle: undefined,
itemClass: undefined,
label: null,
labelStyle: undefined,
size: 'middle',
span: 1,
});
const BORDERED_PADDING: Record<DescriptionsSize, string> = {
large: 'px-6 py-4',
middle: 'px-4 py-2.5',
small: 'px-3 py-2',
};
const PLAIN_PADDING: Record<DescriptionsSize, string> = {
large: 'pb-6',
middle: 'pb-4',
small: 'pb-2',
};
// 冒号通过伪元素追加,避免标签为渲染函数时无法拼接
const COLON_CLASS = "after:content-[':']";
const hasLabel = computed(
() => props.label !== null && props.label !== undefined,
);
const hasContent = computed(
() => props.content !== null && props.content !== undefined,
);
// 数字 0 会被 VbenRenderContent 当作 falsy 隐藏,这里转为字符串保证展示;
// 同时将 null 归一为 undefined,匹配 VbenRenderContent 的 content 类型
const displayLabel = computed(() => {
if (props.label === null || props.label === undefined) return undefined;
return typeof props.label === 'number' ? String(props.label) : props.label;
});
const displayContent = computed(() => {
if (props.content === null || props.content === undefined) return undefined;
return typeof props.content === 'number'
? String(props.content)
: props.content;
});
const cellClass = computed(() => {
if (props.bordered) {
return cn(
'border border-border align-top break-words',
BORDERED_PADDING[props.size],
props.type === 'label'
? 'bg-muted/50 text-start font-normal text-foreground'
: 'text-foreground',
props.itemClass,
);
}
return cn('align-top', PLAIN_PADDING[props.size], props.itemClass);
});
const labelClass = computed(() =>
cn('mr-2 shrink-0 text-muted-foreground', props.colon && COLON_CLASS),
);
</script>
<template>
<component :is="tag" :class="cellClass" :colspan="span">
<!-- 边框模式:每个单元格仅承载 label 或 content -->
<template v-if="bordered">
<span v-if="hasLabel" :style="labelStyle">
<VbenRenderContent :content="displayLabel" />
</span>
<span v-if="hasContent" :style="contentStyle">
<VbenRenderContent :content="displayContent" />
</span>
</template>
<!-- 非边框模式:label + content 容器 -->
<div v-else class="flex">
<span v-if="hasLabel" :class="labelClass" :style="labelStyle">
<VbenRenderContent :content="displayLabel" />
</span>
<span
v-if="hasContent"
class="break-words text-foreground"
:style="contentStyle"
>
<VbenRenderContent :content="displayContent" />
</span>
</div>
</component>
</template>

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import type { PropType } from 'vue';
import type { DescriptionsItemSpan, DescriptionsRenderNode } from './types';
import { defineComponent } from 'vue';
import { DESCRIPTIONS_ITEM_NAME } from './use-descriptions';
/**
* 子节点用法的标记组件,本身不渲染任何内容。
* 其 props 与默认插槽会被父级 VbenDescriptions 收集为列表项。
*/
const VbenDescriptionsItem = defineComponent({
name: DESCRIPTIONS_ITEM_NAME,
props: {
content: {
default: undefined,
type: [
String,
Number,
Function,
Object,
] as PropType<DescriptionsRenderNode>,
},
contentStyle: {
default: undefined,
type: Object,
},
label: {
default: undefined,
type: [
String,
Number,
Function,
Object,
] as PropType<DescriptionsRenderNode>,
},
labelStyle: {
default: undefined,
type: Object,
},
span: {
default: undefined,
type: [Number, String, Object] as PropType<DescriptionsItemSpan>,
},
},
setup() {
return () => null;
},
});
// 额外标记,便于在 vnode 中稳健识别
(VbenDescriptionsItem as Record<string, any>).__isDescriptionsItem = true;
export default VbenDescriptionsItem;
</script>

View File

@@ -0,0 +1,112 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import type { DescriptionsSize, InternalDescriptionsItem } from './types';
import DescriptionsCell from './descriptions-cell.vue';
interface Props {
bordered?: boolean;
colon?: boolean;
contentStyle?: CSSProperties;
labelStyle?: CSSProperties;
row: InternalDescriptionsItem[];
size?: DescriptionsSize;
vertical?: boolean;
}
withDefaults(defineProps<Props>(), {
bordered: false,
colon: true,
contentStyle: undefined,
labelStyle: undefined,
size: 'middle',
vertical: false,
});
function mergeStyle(
base?: CSSProperties,
override?: CSSProperties,
): CSSProperties | undefined {
if (!base && !override) return undefined;
return { ...base, ...override };
}
</script>
<template>
<!-- 垂直布局:标签独占一行,内容独占一行 -->
<template v-if="vertical">
<tr>
<DescriptionsCell
v-for="(item, index) in row"
:key="`label-${item.key ?? index}`"
tag="th"
type="label"
:span="item.span ?? 1"
:bordered="bordered"
:colon="colon"
:size="size"
:label="item.label ?? null"
:item-class="item.class"
:label-style="mergeStyle(labelStyle, item.labelStyle)"
/>
</tr>
<tr>
<DescriptionsCell
v-for="(item, index) in row"
:key="`content-${item.key ?? index}`"
tag="td"
type="content"
:span="item.span ?? 1"
:bordered="bordered"
:size="size"
:content="item.content ?? null"
:item-class="item.class"
:content-style="mergeStyle(contentStyle, item.contentStyle)"
/>
</tr>
</template>
<!-- 水平 + 边框:每项拆分为 label(th) 与 content(td) -->
<tr v-else-if="bordered">
<template v-for="(item, index) in row" :key="item.key ?? index">
<DescriptionsCell
tag="th"
type="label"
:span="1"
:bordered="true"
:size="size"
:label="item.label ?? null"
:item-class="item.class"
:label-style="mergeStyle(labelStyle, item.labelStyle)"
/>
<DescriptionsCell
tag="td"
type="content"
:span="(item.span ?? 1) * 2 - 1"
:bordered="true"
:size="size"
:content="item.content ?? null"
:content-style="mergeStyle(contentStyle, item.contentStyle)"
/>
</template>
</tr>
<!-- 水平 + 非边框:每项一个单元格,label 与 content 同列 -->
<tr v-else>
<DescriptionsCell
v-for="(item, index) in row"
:key="item.key ?? index"
tag="td"
type="item"
:span="item.span ?? 1"
:colon="colon"
:size="size"
:label="item.label ?? null"
:content="item.content ?? null"
:item-class="item.class"
:label-style="mergeStyle(labelStyle, item.labelStyle)"
:content-style="mergeStyle(contentStyle, item.contentStyle)"
/>
</tr>
</template>

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import type { VNode } from 'vue';
import type { DescriptionsItemType, DescriptionsProps } from './types';
import { computed, useSlots } from 'vue';
import { cn } from '@vben-core/shared/utils';
import DescriptionsRow from './descriptions-row.vue';
import {
calcRows,
normalizeItems,
parseItemsFromSlot,
resolveColumn,
useScreens,
} from './use-descriptions';
defineOptions({ name: 'VbenDescriptions' });
const props = withDefaults(defineProps<DescriptionsProps>(), {
bordered: false,
class: undefined,
colon: true,
column: undefined,
contentStyle: undefined,
extra: undefined,
items: undefined,
labelStyle: undefined,
layout: 'horizontal',
size: 'middle',
title: undefined,
});
const slots = useSlots();
const screens = useScreens();
// 优先使用 items;否则从默认插槽中解析 VbenDescriptionsItem
const resolvedItems = computed<DescriptionsItemType[]>(() => {
if (props.items && props.items.length > 0) return props.items;
const nodes = (slots.default?.() ?? []) as VNode[];
return parseItemsFromSlot(nodes);
});
const mergedColumn = computed(() => resolveColumn(props.column, screens.value));
const mergedItems = computed(() =>
normalizeItems(resolvedItems.value, screens.value),
);
const rows = computed(() => calcRows(mergedItems.value, mergedColumn.value));
const hasHeader = computed(
() => !!props.title || !!props.extra || !!slots.title || !!slots.extra,
);
const tableClass = computed(() =>
cn(
'w-full table-auto border-collapse text-sm',
// 非边框模式下,去掉最后一行的底部间距
!props.bordered && '[&>tbody>tr:last-child>td]:pb-0',
),
);
</script>
<template>
<div :class="cn('w-full', props.class)">
<div v-if="hasHeader" class="mb-5 flex items-center justify-between gap-4">
<div class="text-base font-semibold text-foreground">
<slot name="title">{{ title }}</slot>
</div>
<div class="text-foreground">
<slot name="extra">{{ extra }}</slot>
</div>
</div>
<table :class="tableClass">
<tbody>
<DescriptionsRow
v-for="(row, index) in rows"
:key="index"
:row="row"
:vertical="layout === 'vertical'"
:bordered="bordered"
:colon="colon"
:size="size"
:label-style="labelStyle"
:content-style="contentStyle"
/>
</tbody>
</table>
</div>
</template>

View File

@@ -0,0 +1,4 @@
export { default as VbenDescriptionsItem } from './descriptions-item.vue';
export { default as VbenDescriptions } from './descriptions.vue';
export * from './types';

View File

@@ -0,0 +1,93 @@
import type { Component, CSSProperties } from 'vue';
/** 响应式断点,与 antdv-next 保持一致 */
export type DescriptionsBreakpoint =
| 'lg'
| 'md'
| 'sm'
| 'xl'
| 'xs'
| 'xxl'
| 'xxxl';
/** 当前命中的断点集合 */
export type ScreenMap = Partial<Record<DescriptionsBreakpoint, boolean>>;
export type DescriptionsLayout = 'horizontal' | 'vertical';
export type DescriptionsSize = 'large' | 'middle' | 'small';
/** 列数,可为固定数字或按断点配置 */
export type DescriptionsColumn =
| number
| Partial<Record<DescriptionsBreakpoint, number>>;
/** 单项跨列,支持固定数字、'filled'(占满当前行剩余)或按断点配置 */
export type DescriptionsItemSpan =
| 'filled'
| number
| Partial<Record<DescriptionsBreakpoint, number>>;
/** 可渲染内容:字符串/数字/渲染函数/组件 */
export type DescriptionsRenderNode = (() => any) | Component | number | string;
export interface DescriptionsItemType {
/** 内容 */
content?: DescriptionsRenderNode;
/** 内容样式 */
contentStyle?: CSSProperties;
/** 唯一 key */
key?: number | string;
/** 标签 */
label?: DescriptionsRenderNode;
/** 标签样式 */
labelStyle?: CSSProperties;
/** 跨列 */
span?: DescriptionsItemSpan;
}
export interface DescriptionsProps {
/** 是否展示边框 */
bordered?: boolean;
class?: any;
/** 是否显示冒号(仅非 bordered 的水平布局生效) */
colon?: boolean;
/** 一行的列数 */
column?: DescriptionsColumn;
/** 统一的内容样式 */
contentStyle?: CSSProperties;
/** 操作区域,位于标题右侧 */
extra?: string;
/** 数据驱动的列表项;不传则读取默认插槽中的 VbenDescriptionsItem */
items?: DescriptionsItemType[];
/** 统一的标签样式 */
labelStyle?: CSSProperties;
/** 布局方式 */
layout?: DescriptionsLayout;
/** 尺寸 */
size?: DescriptionsSize;
/** 标题 */
title?: string;
}
export interface DescriptionsItemProps {
content?: DescriptionsRenderNode;
contentStyle?: CSSProperties;
label?: DescriptionsRenderNode;
labelStyle?: CSSProperties;
span?: DescriptionsItemSpan;
}
/** 归一化后的内部项,span 已解析为数字 */
export interface InternalDescriptionsItem {
_index?: number;
class?: string;
content?: DescriptionsRenderNode;
contentStyle?: CSSProperties;
filled?: boolean;
key?: number | string;
label?: DescriptionsRenderNode;
labelStyle?: CSSProperties;
span?: number;
style?: CSSProperties;
}

View File

@@ -0,0 +1,216 @@
import type { VNode } from 'vue';
import type {
DescriptionsBreakpoint,
DescriptionsColumn,
DescriptionsItemType,
InternalDescriptionsItem,
ScreenMap,
} from './types';
import { Comment, computed, Fragment } from 'vue';
import { useBreakpoints } from '@vueuse/core';
/** 默认列数映射 */
export const DEFAULT_COLUMN_MAP: Record<DescriptionsBreakpoint, number> = {
lg: 3,
md: 3,
sm: 2,
xl: 3,
xs: 1,
xxl: 3,
xxxl: 4,
};
/** 由大到小的断点顺序,matchScreen 按此顺序取第一个命中的值 */
const RESPONSIVE_ARRAY: DescriptionsBreakpoint[] = [
'xxxl',
'xxl',
'xl',
'lg',
'md',
'sm',
'xs',
];
/** 断点像素值 */
const BREAKPOINT_PX = {
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1600,
xxxl: 2000,
};
/**
* 在给定的断点配置中,按由大到小的顺序取第一个命中的值
*/
export function matchScreen(
screens: ScreenMap,
screenSizes?: Partial<Record<DescriptionsBreakpoint, number>>,
): number | undefined {
if (!screenSizes) return undefined;
for (const breakpoint of RESPONSIVE_ARRAY) {
if (screens[breakpoint] && screenSizes[breakpoint] !== undefined) {
return screenSizes[breakpoint];
}
}
return undefined;
}
/**
* 监听视口宽度,返回当前命中的断点集合
*/
export function useScreens() {
const breakpoints = useBreakpoints(BREAKPOINT_PX);
return computed<ScreenMap>(() => ({
lg: breakpoints.lg.value,
md: breakpoints.md.value,
sm: breakpoints.sm.value,
xl: breakpoints.xl.value,
xs: !breakpoints.sm.value,
xxl: breakpoints.xxl.value,
xxxl: breakpoints.xxxl.value,
}));
}
/**
* 计算最终列数:固定数字直接返回,否则按断点解析
*/
export function resolveColumn(
column: DescriptionsColumn | undefined,
screens: ScreenMap,
): number {
if (typeof column === 'number') return column;
return matchScreen(screens, { ...DEFAULT_COLUMN_MAP, ...column }) ?? 3;
}
/**
* 归一化列表项:将 span 解析为数字,'filled' 标记为 filled
*/
export function normalizeItems(
items: DescriptionsItemType[],
screens: ScreenMap,
): InternalDescriptionsItem[] {
return items.map((item, index) => {
const { span, ...rest } = item;
if (span === 'filled') {
return { ...rest, _index: index, filled: true };
}
return {
...rest,
_index: index,
span: typeof span === 'number' ? span : matchScreen(screens, span),
};
});
}
/**
* 行装箱算法:根据列数与各项 span 将列表项拆分为多行,
* 并补齐每行最后一项以占满列数。移植自 antdv-next useRow。
*/
export function calcRows(
items: InternalDescriptionsItem[],
column: number,
): InternalDescriptionsItem[][] {
let rows: InternalDescriptionsItem[][] = [];
let tmpRow: InternalDescriptionsItem[] = [];
let count = 0;
items.filter(Boolean).forEach((item) => {
const { filled, ...rest } = item;
// filled:占满当前行剩余,并立即换行
if (filled) {
tmpRow.push(rest);
rows.push(tmpRow);
tmpRow = [];
count = 0;
return;
}
const restSpan = column - count;
count += item.span || 1;
if (count >= column) {
// 超出列数时,将当前项 span 收敛为剩余列数,避免溢出
tmpRow.push(count > column ? { ...rest, span: restSpan } : rest);
rows.push(tmpRow);
tmpRow = [];
count = 0;
} else {
tmpRow.push(rest);
}
});
if (tmpRow.length > 0) rows.push(tmpRow);
// 补齐:若一行总 span 不足列数,扩展最后一项
rows = rows.map((row) => {
const total = row.reduce((acc, item) => acc + (item.span || 1), 0);
if (total < column) {
const last = row[row.length - 1];
if (last) {
last.span = column - (total - (last.span || 1));
}
}
return row;
});
return rows;
}
/** 标记组件类型为 DescriptionsItem,便于从插槽 vnode 中识别 */
export const DESCRIPTIONS_ITEM_NAME = 'VbenDescriptionsItem';
function isItemVNode(node: VNode): boolean {
const type = node.type as any;
return (
!!type &&
(type.__isDescriptionsItem === true || type.name === DESCRIPTIONS_ITEM_NAME)
);
}
function flattenVNodes(nodes: VNode[]): VNode[] {
const result: VNode[] = [];
for (const node of nodes) {
if (node.type === Fragment && Array.isArray(node.children)) {
result.push(...flattenVNodes(node.children as VNode[]));
} else if (node.type !== Comment) {
result.push(node);
}
}
return result;
}
/**
* 从默认插槽的 vnode 中解析出列表项,支持
* <VbenDescriptionsItem label="..." :span="2">content</VbenDescriptionsItem> 写法
*/
export function parseItemsFromSlot(nodes: VNode[]): DescriptionsItemType[] {
return flattenVNodes(nodes)
.filter((node) => isItemVNode(node))
.map((node) => {
const props = (node.props ?? {}) as Record<string, any>;
const children = (node.children ?? {}) as Record<string, any>;
const labelSlot =
typeof children.label === 'function' ? children.label : undefined;
const contentDefaultSlot =
typeof children.default === 'function' ? children.default : undefined;
const contentSlot =
typeof children.content === 'function'
? children.content
: contentDefaultSlot;
return {
class: props.class,
content: contentSlot ?? props.content,
contentStyle: props.contentStyle ?? props['content-style'],
key: node.key ?? undefined,
label: labelSlot ?? props.label,
labelStyle: props.labelStyle ?? props['label-style'],
span: props.span,
style: props.style,
} as DescriptionsItemType;
});
}

View File

@@ -6,6 +6,7 @@ export * from './checkbox';
export * from './collapsible';
export * from './context-menu';
export * from './count-to-animator';
export * from './descriptions';
export * from './dropdown-menu';
export * from './expandable-arrow';
export * from './full-screen';
@@ -21,4 +22,5 @@ export * from './segmented';
export * from './select';
export * from './spine-text';
export * from './spinner';
export * from './table-action';
export * from './tooltip';

View File

@@ -3,7 +3,7 @@ import type { PinInputProps } from './types';
import { computed, onBeforeUnmount, ref, useId, watch } from 'vue';
import { PinInput, PinInputGroup, PinInputInput } from '../../ui';
import { PinInput, PinInputGroup, PinInputSlot } from '../../ui';
import { VbenButton } from '../button';
defineOptions({
@@ -101,7 +101,7 @@ const pinType = 'text' as const;
>
<div class="relative flex w-full">
<PinInputGroup class="mr-2">
<PinInputInput
<PinInputSlot
v-for="(item, index) in codeLength"
:key="item"
:index="index"

View File

@@ -115,7 +115,7 @@ function handleScroll(event: Event) {
></div>
<ScrollBar
v-if="horizontal"
:class="scrollBarClass"
:class="cn(scrollBarClass)"
orientation="horizontal"
/>
</ScrollArea>

View File

@@ -45,7 +45,7 @@ function activeClass(tab: string): string[] {
<Tabs v-model="activeTab" :default-value="getDefaultValue">
<TabsList
:style="tabsStyle"
class="bg-accent outline-heavy! relative grid w-full outline! outline-2!"
class="bg-accent outline-heavy! relative grid w-full outline-2!"
>
<TabsIndicator :style="tabsIndicatorStyle" />
<template v-for="tab in tabs" :key="tab.value">

View File

@@ -0,0 +1,116 @@
<script setup lang="ts">
import type { ActionItem } from './types';
import { computed, ref } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import { cn } from '@vben-core/shared/utils';
import {
DropdownMenuItem,
Popover,
PopoverContent,
PopoverTrigger,
} from '../../ui';
import { VbenButton } from '../button';
import { VbenIcon } from '../icon';
const props = defineProps<{ action: ActionItem }>();
const emit = defineEmits<{ confirm: [] }>();
const { $t } = useSimpleLocale();
const open = ref(false);
const itemClass = computed(() =>
cn(
'cursor-pointer gap-2',
props.action.danger && 'text-destructive focus:text-destructive',
),
);
/**
* 阻止 reka-ui 事件的默认行为,用于:
* - @select:阻止点击菜单项后自动关闭菜单,以便弹出气泡确认框;
* - @open-auto-focus:阻止弹层抢占焦点(避免与菜单的焦点陷阱冲突);
* - @focus-outside:阻止因菜单夺回焦点而被误判为「焦点移出」从而关闭弹层。
*/
function preventDefault(event: Event) {
event.preventDefault();
}
function onClick() {
if (props.action.disabled) return;
props.action.onClick?.();
}
function onConfirm() {
open.value = false;
const pc = props.action.popConfirm;
if (pc?.confirm) {
pc.confirm();
} else {
props.action.onClick?.();
}
// 确认后关闭整个下拉菜单
emit('confirm');
}
function onCancel() {
open.value = false;
}
</script>
<template>
<!--
气泡确认:菜单项同时作为 Popover 触发器。
通过双重 as-child(DropdownMenuItem + PopoverTrigger 均合并到同一个叶子元素),
使该元素既是菜单项又是弹层触发器;@select 阻止点击后菜单自动关闭。
-->
<Popover v-if="action.popConfirm" v-model:open="open">
<PopoverTrigger as-child>
<DropdownMenuItem
as-child
:class="itemClass"
:disabled="action.disabled"
@select="preventDefault"
>
<div>
<VbenIcon v-if="action.icon" :icon="action.icon" class="size-4" />
{{ action.text }}
</div>
</DropdownMenuItem>
</PopoverTrigger>
<PopoverContent
class="z-popup w-60"
side="left"
@focus-outside="preventDefault"
@open-auto-focus="preventDefault"
>
<div class="text-foreground mb-3 text-sm">
{{ action.popConfirm.title ?? $t('confirmTitle') }}
</div>
<div class="flex justify-end gap-2">
<VbenButton size="sm" variant="outline" @click="onCancel">
{{ action.popConfirm.cancelText ?? $t('cancel') }}
</VbenButton>
<VbenButton
:variant="action.danger ? 'destructive' : 'default'"
size="sm"
@click="onConfirm"
>
{{ action.popConfirm.okText ?? $t('confirm') }}
</VbenButton>
</div>
</PopoverContent>
</Popover>
<!-- 普通下拉项 -->
<DropdownMenuItem
v-else
:class="itemClass"
:disabled="action.disabled"
@click="onClick"
>
<VbenIcon v-if="action.icon" :icon="action.icon" class="size-4" />
{{ action.text }}
</DropdownMenuItem>
</template>

View File

@@ -0,0 +1,97 @@
<script setup lang="ts">
import type { ActionItem } from './types';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Popover, PopoverContent, PopoverTrigger } from '../../ui';
import { VbenButton } from '../button';
import { VbenIcon } from '../icon';
const props = defineProps<{ action: ActionItem }>();
const open = ref(false);
const buttonClass = computed(() =>
cn(
'gap-1',
props.action.danger && 'text-destructive hover:text-destructive',
props.action.class,
),
);
const variant = computed(() => props.action.variant ?? 'link');
const size = computed(() => props.action.size ?? 'default');
function onClick() {
if (props.action.disabled || props.action.loading) return;
props.action.onClick?.();
}
function onConfirm() {
open.value = false;
const pc = props.action.popConfirm;
if (pc?.confirm) {
pc.confirm();
} else {
props.action.onClick?.();
}
}
function onCancel() {
open.value = false;
}
</script>
<template>
<!-- 气泡确认 -->
<Popover v-if="action.popConfirm" v-model:open="open">
<PopoverTrigger as-child>
<VbenButton
:class="buttonClass"
:disabled="action.disabled"
:loading="action.loading"
:size="size"
class="p-2"
:variant="variant"
>
<VbenIcon :icon="action.icon" v-if="action.icon" class="size-4" />
<span v-if="action.text">{{ action.text }}</span>
</VbenButton>
</PopoverTrigger>
<PopoverContent class="z-popup w-60" side="top">
<div class="text-foreground mb-3 text-sm">
{{ action.popConfirm.title ?? 'Are you sure?' }}
</div>
<div class="flex justify-end gap-2">
<VbenButton size="default" variant="outline" @click="onCancel">
{{ action.popConfirm.cancelText ?? 'Cancel' }}
</VbenButton>
<VbenButton
:variant="action.danger ? 'destructive' : 'default'"
size="default"
class="p-2"
@click="onConfirm"
>
{{ action.popConfirm.okText ?? 'OK' }}
</VbenButton>
</div>
</PopoverContent>
</Popover>
<!-- 普通按钮 -->
<VbenButton
v-else
:class="buttonClass"
:disabled="action.disabled"
:loading="action.loading"
:size="size"
class="p-2"
:variant="variant"
@click="onClick"
>
<VbenIcon :icon="action.icon" v-if="action.icon" class="size-4" />
<span v-if="action.text">{{ action.text }}</span>
</VbenButton>
</template>

View File

@@ -0,0 +1,3 @@
export { default as VbenTableAction } from './table-action.vue';
export type * from './types';

View File

@@ -0,0 +1,216 @@
<script setup lang="ts">
import type { ActionItem, TableActionProps } from './types';
import { computed, ref } from 'vue';
import { Ellipsis } from '@vben-core/icons';
import { cn } from '@vben-core/shared/utils';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
Separator,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '../../ui';
import { VbenButton } from '../button';
import { VbenIcon } from '../icon';
import ActionDropdownItemComp from './action-dropdown-item.vue';
import ActionItemComp from './action-item.vue';
defineOptions({ name: 'VbenTableAction' });
const props = withDefaults(defineProps<TableActionProps>(), {
actions: () => [],
align: 'end',
class: undefined,
divider: false,
dropdownActions: () => [],
hasPermission: undefined,
moreText: undefined,
});
function checkVisible(item: ActionItem): boolean {
// 权限
if (item.auth && props.hasPermission && !props.hasPermission(item.auth)) {
return false;
}
// ifShow
if (typeof item.ifShow === 'boolean') return item.ifShow;
if (typeof item.ifShow === 'function') return item.ifShow();
return true;
}
const visibleActions = computed(() =>
(props.actions ?? []).filter((item) => checkVisible(item)),
);
const visibleDropdownActions = computed(() =>
(props.dropdownActions ?? []).filter((item) => checkVisible(item)),
);
const alignClass = computed(
() =>
({ center: 'justify-center', end: 'justify-end', start: 'justify-start' })[
props.align
],
);
// 缓存根节点类名,避免每次渲染都执行 cn()(内部 tailwind-merge 解析开销较大)
const wrapperClass = computed(() =>
cn('flex items-center gap-1', alignClass.value, props.class),
);
function tooltipSide(action: ActionItem) {
return typeof action.tooltip === 'object'
? (action.tooltip.side ?? 'top')
: 'top';
}
function tooltipContent(action: ActionItem) {
return typeof action.tooltip === 'object'
? action.tooltip.content
: action.tooltip;
}
/**
* 预计算每个主操作的渲染视图模型:
* - 普通按钮在本组件内直接渲染,不再为每个操作多包一层子组件,
* 表格大量行时可显著减少组件实例数;
* - 仅 popConfirm 操作仍交由子组件维护独立弹层状态;
* - 类名等在此一次性计算并缓存,避免模板每次渲染都执行 cn()。
*/
const renderedActions = computed(() => {
const list = visibleActions.value;
return list.map((action, index) => {
const hasTooltip = !!action.tooltip && !action.popConfirm;
return {
action,
buttonClass: cn(
'gap-1 p-2',
action.danger && 'text-destructive hover:text-destructive',
action.class,
),
hasTooltip,
isConfirm: !!action.popConfirm,
key: action.key ?? index,
showDivider: props.divider && index < list.length - 1,
size: action.size ?? 'default',
tooltipContent: hasTooltip ? tooltipContent(action) : undefined,
tooltipSide: hasTooltip ? tooltipSide(action) : 'top',
variant: action.variant ?? 'link',
};
});
});
const dropdownOpen = ref(false);
function onActionClick(action: ActionItem) {
if (action.disabled || action.loading) return;
action.onClick?.();
}
/**
* 当与气泡确认(Popover)交互时,避免误关闭整个下拉菜单。
* Popover 内容被 Portal 渲染到菜单之外,默认会被判定为「点击外部」而关闭菜单。
*/
function onContentInteractOutside(event: Event) {
const target = (event as CustomEvent).detail?.originalEvent?.target as
| HTMLElement
| null
| undefined;
if (target?.closest('[data-slot="popover-content"]')) {
event.preventDefault();
}
}
</script>
<template>
<div :class="wrapperClass">
<!-- 所有主操作共享同一个 TooltipProvider,避免每个 tooltip 各建一个 provider -->
<TooltipProvider v-if="renderedActions.length > 0" :delay-duration="0">
<template v-for="item in renderedActions" :key="item.key">
<!-- 气泡确认:需独立弹层状态,交由子组件维护 -->
<ActionItemComp v-if="item.isConfirm" :action="item.action" />
<!-- 带提示的普通按钮 -->
<Tooltip v-else-if="item.hasTooltip">
<TooltipTrigger as-child tabindex="-1">
<VbenButton
:class="item.buttonClass"
:disabled="item.action.disabled"
:loading="item.action.loading"
:size="item.size"
:variant="item.variant"
@click="onActionClick(item.action)"
>
<VbenIcon
v-if="item.action.icon"
:icon="item.action.icon"
class="size-4"
/>
<span v-if="item.action.text">{{ item.action.text }}</span>
</VbenButton>
</TooltipTrigger>
<TooltipContent
:side="item.tooltipSide"
class="side-content bg-accent text-popover-foreground rounded-md"
>
{{ item.tooltipContent }}
</TooltipContent>
</Tooltip>
<!-- 普通按钮 -->
<VbenButton
v-else
:class="item.buttonClass"
:disabled="item.action.disabled"
:loading="item.action.loading"
:size="item.size"
:variant="item.variant"
@click="onActionClick(item.action)"
>
<VbenIcon
v-if="item.action.icon"
:icon="item.action.icon"
class="size-4"
/>
<span v-if="item.action.text">{{ item.action.text }}</span>
</VbenButton>
<Separator v-if="item.showDivider" orientation="vertical" class="h-4" />
</template>
</TooltipProvider>
<DropdownMenu
v-if="visibleDropdownActions.length > 0"
v-model:open="dropdownOpen"
>
<DropdownMenuTrigger as-child>
<VbenButton class="gap-1 p-2" variant="link">
<Ellipsis class="size-4" />
<span v-if="moreText">{{ moreText }}</span>
</VbenButton>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
@interact-outside="onContentInteractOutside"
>
<template
v-for="(item, index) in visibleDropdownActions"
:key="item.key ?? index"
>
<ActionDropdownItemComp
:action="item"
@confirm="dropdownOpen = false"
/>
<DropdownMenuSeparator
v-if="divider && index < visibleDropdownActions.length - 1"
/>
</template>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>

View File

@@ -0,0 +1,75 @@
import type { ButtonVariants } from '../../ui';
import { VbenIcon } from '../icon';
/** 权限码:单个或多个,配合注入的 hasPermission 判断 */
export type TableActionAuth = string | string[];
/** 操作按钮提示 */
export interface TableActionTooltip {
content: string;
side?: 'bottom' | 'left' | 'right' | 'top';
}
/** 气泡确认框配置 */
export interface TableActionPopConfirm {
/** 取消按钮文案 */
cancelText?: string;
/** 确认回调;未提供时回退到 action.onClick */
confirm?: () => void;
/** 确认按钮文案 */
okText?: string;
/** 提示标题 */
title?: string;
}
export interface ActionItem {
/** 权限码,配合注入的 hasPermission 过滤 */
auth?: TableActionAuth;
/** 自定义类名 */
class?: any;
/** 危险操作(红色文字) */
danger?: boolean;
/** 是否禁用 */
disabled?: boolean;
/** 图标组件 */
icon?: typeof VbenIcon.icon;
/** 是否显示:布尔或返回布尔的函数 */
ifShow?: (() => boolean) | boolean;
/** 唯一标识,点击回调可据此区分 */
key?: number | string;
/** 加载状态 */
loading?: boolean;
/** 点击回调 */
onClick?: () => void;
/** 气泡确认框 */
popConfirm?: TableActionPopConfirm;
/** 尺寸 */
size?: ButtonVariants['size'];
/** 文本 */
text?: string;
/** 提示:字符串或配置对象 */
tooltip?: string | TableActionTooltip;
/** 按钮样式变体 */
variant?: ButtonVariants['variant'];
}
export interface TableActionProps {
/** 主操作按钮 */
actions?: ActionItem[];
/** 对齐方式 */
align?: 'center' | 'end' | 'start';
/** 自定义类名 */
class?: any;
/** 按钮之间是否显示分割线 */
divider?: boolean;
/** “更多”下拉中的操作 */
dropdownActions?: ActionItem[];
/**
* 权限判断函数,返回 false 则隐藏对应 auth 的操作。
* 核心组件不依赖业务,由使用方注入(如 useAccess().hasAccessByCodes)。
*/
hasPermission?: (auth?: TableActionAuth) => boolean;
/** “更多”按钮文案(提供时显示在图标右侧) */
moreText?: string;
}

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
import { CircleHelp } from 'lucide-vue-next';
import { CircleHelp } from '@lucide/vue';
import Tooltip from './tooltip.vue';

View File

@@ -10,7 +10,7 @@ const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot></slot>
<AccordionRoot v-slot="slotProps" data-slot="accordion" v-bind="forwarded">
<slot v-bind="slotProps"></slot>
</AccordionRoot>
</template>

View File

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

View File

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

View File

@@ -1,29 +1,29 @@
<script setup lang="ts">
import type { AccordionTriggerProps } from 'reka-ui';
import { computed } from 'vue';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ChevronDown } from 'lucide-vue-next';
import { ChevronDown } from '@lucide/vue';
import { reactiveOmit } from '@vueuse/core';
import { AccordionHeader, AccordionTrigger } from 'reka-ui';
const props = defineProps<AccordionTriggerProps & { class?: any }>();
const props = defineProps<
AccordionTriggerProps & { class?: HTMLAttributes['class'] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
data-slot="accordion-trigger"
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',
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
@@ -31,7 +31,7 @@ const delegatedProps = computed(() => {
<slot></slot>
<slot name="icon">
<ChevronDown
class="text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200"
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
/>
</slot>
</AccordionTrigger>

View File

@@ -10,7 +10,11 @@ const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AlertDialogRoot v-bind="forwarded">
<slot></slot>
<AlertDialogRoot
v-slot="slotProps"
data-slot="alert-dialog"
v-bind="forwarded"
>
<slot v-bind="slotProps"></slot>
</AlertDialogRoot>
</template>

View File

@@ -1,13 +1,27 @@
<script setup lang="ts">
import type { AlertDialogActionProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { reactiveOmit } from '@vueuse/core';
import { AlertDialogAction } from 'reka-ui';
const props = defineProps<AlertDialogActionProps>();
import { buttonVariants } from '../button';
const props = defineProps<
AlertDialogActionProps & { class?: HTMLAttributes['class'] }
>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<AlertDialogAction v-bind="props">
<AlertDialogAction
v-bind="delegatedProps"
:class="cn(buttonVariants(), props.class)"
>
<slot></slot>
</AlertDialogAction>
</template>

View File

@@ -1,13 +1,29 @@
<script setup lang="ts">
import type { AlertDialogCancelProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { reactiveOmit } from '@vueuse/core';
import { AlertDialogCancel } from 'reka-ui';
const props = defineProps<AlertDialogCancelProps>();
import { buttonVariants } from '../button';
const props = defineProps<
AlertDialogCancelProps & { class?: HTMLAttributes['class'] }
>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<AlertDialogCancel v-bind="props">
<AlertDialogCancel
v-bind="delegatedProps"
:class="
cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)
"
>
<slot></slot>
</AlertDialogCancel>
</template>

View File

@@ -3,17 +3,21 @@ import type { AlertDialogContentEmits, AlertDialogContentProps } from 'reka-ui';
import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { reactiveOmit } from '@vueuse/core';
import {
AlertDialogContent,
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from 'reka-ui';
import AlertDialogOverlay from './AlertDialogOverlay.vue';
defineOptions({
inheritAttrs: false,
});
const props = withDefaults(
defineProps<
@@ -32,11 +36,7 @@ const emits = defineEmits<
AlertDialogContentEmits & { close: []; closed: []; opened: [] }
>();
const delegatedProps = computed(() => {
const { class: _, modal: _modal, open: _open, ...delegated } = props;
return delegated;
});
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
@@ -60,6 +60,8 @@ defineExpose({
<AlertDialogPortal>
<Transition name="fade" appear>
<AlertDialogOverlay
data-slot="alert-dialog-overlay"
class="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-50 bg-black/80"
v-if="open && modal"
:style="{
...(zIndex ? { zIndex } : {}),
@@ -71,15 +73,14 @@ defineExpose({
/>
</Transition>
<AlertDialogContent
data-slot="alert-dialog-content"
ref="contentRef"
:style="{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }"
@animationend="onAnimationEnd"
v-bind="forwarded"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'z-popup bg-background p-6 shadow-lg outline-hidden 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',
'z-popup 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
{
'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':
!centered,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,4 +3,7 @@ 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

@@ -1,27 +1,25 @@
<script setup lang="ts">
import type { AvatarVariants } from './avatar';
import type { HTMLAttributes } from 'vue';
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',
},
);
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
<AvatarRoot
data-slot="avatar"
:class="
cn(
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
props.class,
)
"
>
<slot></slot>
</AvatarRoot>
</template>

View File

@@ -1,13 +1,31 @@
<script setup lang="ts">
import type { AvatarFallbackProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { reactiveOmit } from '@vueuse/core';
import { AvatarFallback } from 'reka-ui';
const props = defineProps<AvatarFallbackProps>();
const props = defineProps<
AvatarFallbackProps & { class?: HTMLAttributes['class'] }
>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<AvatarFallback v-bind="props">
<AvatarFallback
data-slot="avatar-fallback"
v-bind="delegatedProps"
:class="
cn(
'bg-muted flex size-full items-center justify-center rounded-full',
props.class,
)
"
>
<slot></slot>
</AvatarFallback>
</template>

View File

@@ -7,5 +7,11 @@ const props = defineProps<AvatarImageProps>();
</script>
<template>
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
<AvatarImage
data-slot="avatar-image"
v-bind="props"
class="aspect-square size-full"
>
<slot></slot>
</AvatarImage>
</template>

View File

@@ -1,22 +0,0 @@
import type { VariantProps } from 'class-variance-authority';
import { 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

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

View File

@@ -1,18 +1,33 @@
<script setup lang="ts">
import type { BadgeVariants } from './badge';
import type { PrimitiveProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import type { BadgeVariants } from '.';
import { cn } from '@vben-core/shared/utils';
import { badgeVariants } from './badge';
import { reactiveOmit } from '@vueuse/core';
import { Primitive } from 'reka-ui';
const props = defineProps<{
class?: any;
variant?: BadgeVariants['variant'];
}>();
import { badgeVariants } from '.';
const props = defineProps<
PrimitiveProps & {
class?: HTMLAttributes['class'];
variant?: BadgeVariants['variant'];
}
>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<Primitive
data-slot="badge"
:class="cn(badgeVariants({ variant }), props.class)"
v-bind="delegatedProps"
>
<slot></slot>
</div>
</Primitive>
</template>

View File

@@ -3,23 +3,23 @@ import type { VariantProps } from 'class-variance-authority';
import { 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',
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
defaultVariants: {
variant: 'default',
},
variants: {
variant: {
default:
'border-transparent bg-accent hover:bg-accent text-primary-foreground shadow-sm',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive-hover',
outline: 'text-foreground',
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
export type BadgeVariants = VariantProps<typeof badgeVariants>;

View File

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

View File

@@ -1,21 +1,24 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { MoreHorizontal } from 'lucide-vue-next';
import { MoreHorizontal } from '@lucide/vue';
const props = defineProps<{
class?: any;
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<span
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
aria-hidden="true"
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
:class="cn('flex size-9 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="h-4 w-4" />
<MoreHorizontal class="size-4" />
</slot>
<span class="sr-only">More</span>
</span>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,21 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ChevronRight } from 'lucide-vue-next';
import { ChevronRight } from '@lucide/vue';
const props = defineProps<{
class?: any;
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<li
:class="cn('[&>svg]:size-3.5', props.class)"
aria-hidden="true"
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
:class="cn('[&>svg]:size-3.5', props.class)"
>
<slot>
<ChevronRight />

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui';
import type { ButtonVariants, ButtonVariantSize } from './types';
import type { HTMLAttributes } from 'vue';
import type { ButtonVariants } from './button';
import { cn } from '@vben-core/shared/utils';
@@ -10,19 +12,21 @@ import { Primitive } from 'reka-ui';
import { buttonVariants } from './button';
interface Props extends PrimitiveProps {
class?: any;
size?: ButtonVariantSize;
variant?: ButtonVariants;
class?: HTMLAttributes['class'];
size?: ButtonVariants['size'];
variant?: ButtonVariants['variant'];
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
class: '',
});
</script>
<template>
<Primitive
data-slot="button"
:data-variant="variant"
:data-size="size"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"

View File

@@ -1,4 +1,4 @@
import { cva } from 'class-variance-authority';
import { cva, type VariantProps } 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',
@@ -32,3 +32,4 @@ export const buttonVariants = cva(
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View File

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

View File

@@ -1,20 +0,0 @@
export type ButtonVariantSize =
| 'default'
| 'icon'
| 'lg'
| 'sm'
| 'xs'
| null
| undefined;
export type ButtonVariants =
| 'default'
| 'destructive'
| 'ghost'
| 'heavy'
| 'icon'
| 'link'
| 'outline'
| 'secondary'
| null
| undefined;

View File

@@ -1,16 +1,19 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
data-slot="card"
:class="
cn(
'border-border bg-card text-card-foreground rounded-xl border',
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
props.class,
)
"

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
data-slot="card-action"
:class="
cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
props.class,
)
"
>
<slot></slot>
</div>
</template>

View File

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

View File

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

View File

@@ -1,13 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<div
data-slot="card-footer"
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
>
<slot></slot>
</div>
</template>

View File

@@ -1,13 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-5', props.class)">
<div
data-slot="card-header"
:class="
cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
props.class,
)
"
>
<slot></slot>
</div>
</template>

View File

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

View File

@@ -1,4 +1,5 @@
export { default as Card } from './Card.vue';
export { default as CardAction } from './CardAction.vue';
export { default as CardContent } from './CardContent.vue';
export { default as CardDescription } from './CardDescription.vue';
export { default as CardFooter } from './CardFooter.vue';

View File

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

View File

@@ -12,7 +12,7 @@ const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<ContextMenuRoot v-bind="forwarded">
<ContextMenuRoot data-slot="context-menu" v-bind="forwarded">
<slot></slot>
</ContextMenuRoot>
</template>

View File

@@ -4,42 +4,46 @@ import type {
ContextMenuCheckboxItemProps,
} from 'reka-ui';
import { computed } from 'vue';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Check } from 'lucide-vue-next';
import { Check } from '@lucide/vue';
import { reactiveOmit } from '@vueuse/core';
import {
ContextMenuCheckboxItem,
ContextMenuItemIndicator,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<ContextMenuCheckboxItemProps & { class?: any }>();
const props = defineProps<
ContextMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }
>();
const emits = defineEmits<ContextMenuCheckboxItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ContextMenuCheckboxItem
data-slot="context-menu-checkbox-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span
class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
>
<ContextMenuItemIndicator>
<Check class="h-4 w-4" />
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</ContextMenuItemIndicator>
</span>
<slot></slot>

View File

@@ -1,24 +1,27 @@
<script setup lang="ts">
import type { ContextMenuContentEmits, ContextMenuContentProps } from 'reka-ui';
import { computed } from 'vue';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { reactiveOmit } from '@vueuse/core';
import {
ContextMenuContent,
ContextMenuPortal,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<ContextMenuContentProps & { class?: any }>();
defineOptions({
inheritAttrs: false,
});
const props = defineProps<
ContextMenuContentProps & { class?: HTMLAttributes['class'] }
>();
const emits = defineEmits<ContextMenuContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
@@ -26,10 +29,11 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template>
<ContextMenuPortal>
<ContextMenuContent
v-bind="forwarded"
data-slot="context-menu-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'z-popup border-border bg-popover text-popover-foreground 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 min-w-32 overflow-hidden rounded-md border p-1 shadow-md',
'z-popup bg-popover text-popover-foreground 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 max-h-(--reka-context-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
props.class,
)
"

View File

@@ -7,7 +7,7 @@ const props = defineProps<ContextMenuGroupProps>();
</script>
<template>
<ContextMenuGroup v-bind="props">
<ContextMenuGroup data-slot="context-menu-group" v-bind="props">
<slot></slot>
</ContextMenuGroup>
</template>

View File

@@ -1,33 +1,41 @@
<script setup lang="ts">
import type { ContextMenuItemEmits, ContextMenuItemProps } from 'reka-ui';
import { computed } from 'vue';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { reactiveOmit } from '@vueuse/core';
import { ContextMenuItem, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<
ContextMenuItemProps & { class?: any; inset?: boolean }
>();
const props = withDefaults(
defineProps<
ContextMenuItemProps & {
class?: HTMLAttributes['class'];
inset?: boolean;
variant?: 'default' | 'destructive';
}
>(),
{
variant: 'default',
},
);
const emits = defineEmits<ContextMenuItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ContextMenuItem
data-slot="context-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
'focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"

View File

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

View File

@@ -7,7 +7,7 @@ const props = defineProps<ContextMenuPortalProps>();
</script>
<template>
<ContextMenuPortal v-bind="props">
<ContextMenuPortal data-slot="context-menu-portal" v-bind="props">
<slot></slot>
</ContextMenuPortal>
</template>

View File

@@ -13,7 +13,10 @@ const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<ContextMenuRadioGroup v-bind="forwarded">
<ContextMenuRadioGroup
data-slot="context-menu-radio-group"
v-bind="forwarded"
>
<slot></slot>
</ContextMenuRadioGroup>
</template>

View File

@@ -4,42 +4,46 @@ import type {
ContextMenuRadioItemProps,
} from 'reka-ui';
import { computed } from 'vue';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Circle } from 'lucide-vue-next';
import { Circle } from '@lucide/vue';
import { reactiveOmit } from '@vueuse/core';
import {
ContextMenuItemIndicator,
ContextMenuRadioItem,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<ContextMenuRadioItemProps & { class?: any }>();
const props = defineProps<
ContextMenuRadioItemProps & { class?: HTMLAttributes['class'] }
>();
const emits = defineEmits<ContextMenuRadioItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ContextMenuRadioItem
data-slot="context-menu-radio-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span
class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
>
<ContextMenuItemIndicator>
<Circle class="h-2 w-2 fill-current" />
<slot name="indicator-icon">
<Circle class="size-2 fill-current" />
</slot>
</ContextMenuItemIndicator>
</span>
<slot></slot>

View File

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

View File

@@ -1,13 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<span
data-slot="context-menu-shortcut"
:class="
cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)
"

View File

@@ -10,7 +10,7 @@ const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<ContextMenuSub v-bind="forwarded">
<ContextMenuSub data-slot="context-menu-sub" v-bind="forwarded">
<slot></slot>
</ContextMenuSub>
</template>

View File

@@ -4,37 +4,30 @@ import type {
DropdownMenuSubContentProps,
} from 'reka-ui';
import { computed } from 'vue';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { reactiveOmit } from '@vueuse/core';
import { ContextMenuSubContent, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<DropdownMenuSubContentProps & { class?: any }>();
const props = defineProps<
DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }
>();
const emits = defineEmits<DropdownMenuSubContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ContextMenuSubContent
data-slot="context-menu-sub-content"
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md',
'border-border border',
'bg-popover text-popover-foreground p-1 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',
'bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--reka-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
props.class,
)
"

View File

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

View File

@@ -9,7 +9,7 @@ const forwardedProps = useForwardProps(props);
</script>
<template>
<ContextMenuTrigger v-bind="forwardedProps">
<ContextMenuTrigger data-slot="context-menu-trigger" v-bind="forwardedProps">
<slot></slot>
</ContextMenuTrigger>
</template>

View File

@@ -10,7 +10,7 @@ const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot></slot>
<DialogRoot v-slot="slotProps" data-slot="dialog" v-bind="forwarded">
<slot v-bind="slotProps"></slot>
</DialogRoot>
</template>

View File

@@ -7,7 +7,7 @@ const props = defineProps<DialogCloseProps>();
</script>
<template>
<DialogClose v-bind="props">
<DialogClose data-slot="dialog-close" v-bind="props">
<slot></slot>
</DialogClose>
</template>

View File

@@ -7,7 +7,7 @@ import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { X } from 'lucide-vue-next';
import { X } from '@lucide/vue';
import { DialogClose, DialogContent, useForwardPropsEmits } from 'reka-ui';
import DialogOverlay from './DialogOverlay.vue';

View File

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

View File

@@ -1,15 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{ class?: any }>();
import { DialogClose } from 'reka-ui';
import { Button } from '../button';
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class'];
showCloseButton?: boolean;
}>(),
{
showCloseButton: false,
},
);
</script>
<template>
<div
data-slot="dialog-footer"
:class="
cn('flex flex-col-reverse flex-row justify-end gap-x-2', props.class)
cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)
"
>
<slot></slot>
<DialogClose v-if="showCloseButton" as-child>
<Button variant="outline"> Close </Button>
</DialogClose>
</div>
</template>

View File

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

View File

@@ -1,11 +1,31 @@
<script setup lang="ts">
import { inject } from 'vue';
import type { DialogOverlayProps } from 'reka-ui';
import { useScrollLock } from '@vben-core/composables';
import type { HTMLAttributes } from 'vue';
useScrollLock();
const id = inject('DISMISSABLE_MODAL_ID');
import { cn } from '@vben-core/shared/utils';
import { reactiveOmit } from '@vueuse/core';
import { DialogOverlay } from 'reka-ui';
const props = defineProps<
DialogOverlayProps & { class?: HTMLAttributes['class'] }
>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<div :data-dismissable-modal="id" class="z-popup bg-overlay inset-0"></div>
<DialogOverlay
data-slot="dialog-overlay"
v-bind="delegatedProps"
:class="
cn(
'z-popup bg-overlay inset-0 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed',
props.class,
)
"
>
<slot></slot>
</DialogOverlay>
</template>

View File

@@ -1,11 +1,10 @@
<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 { X } from '@lucide/vue';
import { reactiveOmit } from '@vueuse/core';
import {
DialogClose,
DialogContent,
@@ -14,17 +13,17 @@ import {
useForwardPropsEmits,
} from 'reka-ui';
defineOptions({
inheritAttrs: false,
});
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 delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
@@ -33,17 +32,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<DialogPortal>
<DialogOverlay
:style="{ zIndex }"
class="border-border data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 absolute inset-0 grid place-items-center overflow-y-auto border bg-black/80"
class="fixed inset-0 grid place-items-center overflow-y-auto 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(
'border-border bg-background relative z-50 my-8 grid w-full max-w-lg gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
'relative z-50 grid w-full max-w-lg my-8 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"
v-bind="{ ...$attrs, ...forwarded }"
@pointer-down-outside="
(event) => {
const originalEvent = event.detail.originalEvent;
@@ -60,9 +58,9 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<slot></slot>
<DialogClose
class="hover:bg-secondary absolute top-4 right-4 rounded-md p-0.5 transition-colors"
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<X class="h-4 w-4" />
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>

View File

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

View File

@@ -7,7 +7,7 @@ const props = defineProps<DialogTriggerProps>();
</script>
<template>
<DialogTrigger v-bind="props">
<DialogTrigger data-slot="dialog-trigger" v-bind="props">
<slot></slot>
</DialogTrigger>
</template>

View File

@@ -4,6 +4,7 @@ export { default as DialogContent } from './DialogContent.vue';
export { default as DialogDescription } from './DialogDescription.vue';
export { default as DialogFooter } from './DialogFooter.vue';
export { default as DialogHeader } from './DialogHeader.vue';
export { default as DialogOverlay } from './DialogOverlay.vue';
export { default as DialogScrollContent } from './DialogScrollContent.vue';
export { default as DialogTitle } from './DialogTitle.vue';
export { default as DialogTrigger } from './DialogTrigger.vue';

View File

@@ -3,16 +3,18 @@ import type { DropdownMenuRootEmits, DropdownMenuRootProps } from 'reka-ui';
import { DropdownMenuRoot, useForwardPropsEmits } from 'reka-ui';
const props = withDefaults(defineProps<DropdownMenuRootProps>(), {
modal: false,
});
const props = defineProps<DropdownMenuRootProps>();
const emits = defineEmits<DropdownMenuRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRoot v-bind="forwarded">
<slot></slot>
<DropdownMenuRoot
v-slot="slotProps"
data-slot="dropdown-menu"
v-bind="forwarded"
>
<slot v-bind="slotProps"></slot>
</DropdownMenuRoot>
</template>

View File

@@ -4,42 +4,46 @@ import type {
DropdownMenuCheckboxItemProps,
} from 'reka-ui';
import { computed } from 'vue';
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { Check } from 'lucide-vue-next';
import { Check } from '@lucide/vue';
import { reactiveOmit } from '@vueuse/core';
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: any }>();
const props = defineProps<
DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }
>();
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuCheckboxItem
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span
class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
>
<DropdownMenuItemIndicator>
<Check class="h-4 w-4" />
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot></slot>

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