perf: optimization of tabbar display (#4169)

* perf: optimization of tabbar display

* fix: ci error

* chore: typo

* chore: typo
pull/48/MERGE
Vben 2024-08-16 22:20:18 +08:00 committed by GitHub
parent 8987067b5a
commit 0faf7810b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 710 additions and 504 deletions

View File

@ -40,11 +40,11 @@
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"ant-design-vue": "^4.2.3",
"dayjs": "^1.11.12",
"pinia": "2.2.2",
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
}
}

View File

@ -40,11 +40,11 @@
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"dayjs": "^1.11.12",
"element-plus": "^2.8.0",
"pinia": "2.2.2",
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
},
"devDependencies": {

View File

@ -40,10 +40,10 @@
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"naive-ui": "^2.39.0",
"pinia": "2.2.2",
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
}
}

View File

@ -14,6 +14,6 @@
"@nolebase/vitepress-plugin-git-changelog": "^2.4.0",
"@vite-pwa/vitepress": "^0.5.0",
"vitepress": "^1.3.2",
"vue": "^3.4.38"
"vue": "^3.4.37"
}
}

View File

@ -42,7 +42,8 @@ export async function typescript(): Promise<Linter.Config[]> {
},
],
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
// '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': [

View File

@ -94,7 +94,7 @@
"node": ">=20",
"pnpm": ">=9"
},
"packageManager": "pnpm@9.7.0",
"packageManager": "pnpm@9.7.1",
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {

View File

@ -35,7 +35,7 @@
},
"dependencies": {
"@iconify/vue": "^4.1.2",
"lucide-vue-next": "^0.427.0",
"vue": "^3.4.38"
"lucide-vue-next": "^0.428.0",
"vue": "^3.4.37"
}
}

View File

@ -56,7 +56,7 @@
},
"dependencies": {
"@ctrl/tinycolor": "^4.1.0",
"@vue/shared": "^3.4.38",
"@vue/shared": "^3.4.37",
"clsx": "^2.1.1",
"defu": "^6.1.4",
"lodash.clonedeep": "^4.5.0",

View File

@ -38,7 +38,7 @@
}
},
"dependencies": {
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
}
}

View File

@ -107,20 +107,23 @@ type MergeAll<
? MergeAll<Rest, Merge<R, F>>
: R;
export {
type AnyFunction,
type AnyNormalFunction,
type AnyPromiseFunction,
type DeepPartial,
type DeepReadonly,
type IntervalHandle,
type MaybeComputedRef,
type MaybeReadonlyRef,
type Merge,
type MergeAll,
type NonNullable,
type Nullable,
type ReadonlyRecordable,
type Recordable,
type TimeoutHandle,
type EmitType = (name: Name, ...args: any[]) => void;
export type {
AnyFunction,
AnyNormalFunction,
AnyPromiseFunction,
DeepPartial,
DeepReadonly,
EmitType,
IntervalHandle,
MaybeComputedRef,
MaybeReadonlyRef,
Merge,
MergeAll,
NonNullable,
Nullable,
ReadonlyRecordable,
Recordable,
TimeoutHandle,
};

View File

@ -36,10 +36,10 @@
},
"dependencies": {
"@vben-core/shared": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"radix-vue": "^1.9.4",
"sortablejs": "^1.15.2",
"vue": "^3.4.38"
"vue": "^3.4.37"
},
"devDependencies": {
"@types/sortablejs": "^1.15.8"

View File

@ -1,5 +1,5 @@
import type { CSSProperties } from 'vue';
import { computed, nextTick, onMounted, ref } from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import {
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
@ -14,6 +14,7 @@ import { useCssVar, useDebounceFn } from '@vueuse/core';
* @zh_CN content style
*/
function useContentStyle() {
let resizeObserver: null | ResizeObserver = null;
const contentElement = ref<HTMLDivElement | null>(null);
const visibleDomRect = ref<null | VisibleDomRect>(null);
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
@ -41,12 +42,15 @@ function useContentStyle() {
);
onMounted(() => {
nextTick(() => {
if (contentElement.value) {
const observer = new ResizeObserver(debouncedCalcHeight);
observer.observe(contentElement.value);
if (contentElement.value && !resizeObserver) {
resizeObserver = new ResizeObserver(debouncedCalcHeight);
resizeObserver.observe(contentElement.value);
}
});
onUnmounted(() => {
resizeObserver?.disconnect();
resizeObserver = null;
});
return { contentElement, overlayStyle, visibleDomRect };

View File

@ -39,7 +39,7 @@ describe('useSortable', () => {
expect(Sortable.default.create).toHaveBeenCalledWith(
mockElement,
expect.objectContaining({
animation: 100,
animation: 300,
delay: 400,
delayOnTouchOnly: true,
...customOptions,

View File

@ -18,7 +18,7 @@ function useSortable<T extends HTMLElement>(
// Sortable?.default?.mount?.(AutoScroll);
const sortable = Sortable?.default?.create?.(sortableContainer, {
animation: 100,
animation: 300,
delay: 400,
delayOnTouchOnly: true,
...options,

View File

@ -31,7 +31,7 @@
"dependencies": {
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1",
"vue": "^3.4.38"
"@vueuse/core": "^11.0.0",
"vue": "^3.4.37"
}
}

View File

@ -41,7 +41,7 @@
"@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1",
"vue": "^3.4.38"
"@vueuse/core": "^11.0.0",
"vue": "^3.4.37"
}
}

View File

@ -42,7 +42,7 @@
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1",
"vue": "^3.4.38"
"@vueuse/core": "^11.0.0",
"vue": "^3.4.37"
}
}

View File

@ -46,10 +46,10 @@
"@vben-core/icons": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"class-variance-authority": "^0.7.0",
"lucide-vue-next": "^0.427.0",
"lucide-vue-next": "^0.428.0",
"radix-vue": "^1.9.4",
"vue": "^3.4.38"
"vue": "^3.4.37"
}
}

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared';
@ -11,6 +11,10 @@ interface Props {
scrollBarClass?: any;
shadow?: boolean;
shadowBorder?: boolean;
shadowBottom?: boolean;
shadowLeft?: boolean;
shadowRight?: boolean;
shadowTop?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
@ -18,29 +22,66 @@ const props = withDefaults(defineProps<Props>(), {
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);
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(() => ({
'shadow-both':
!isAtLeft.value &&
!isAtRight.value &&
showShadowLeft.value &&
showShadowRight.value,
'shadow-left': !isAtLeft.value && showShadowLeft.value,
'shadow-right': !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 offsetHeight = target?.offsetHeight ?? 0;
const offsetWidth = target?.offsetWidth ?? 0;
const scrollHeight = target?.scrollHeight ?? 0;
const scrollWidth = target?.scrollWidth ?? 0;
isAtTop.value = scrollTop <= 0;
isAtLeft.value = scrollLeft <= 0;
isAtBottom.value = scrollTop + offsetHeight >= scrollHeight;
isAtRight.value = scrollLeft + offsetWidth >= scrollWidth;
emit('scrollAt', {
bottom: isAtBottom.value,
left: isAtLeft.value,
right: isAtRight.value,
top: isAtTop.value,
});
}
</script>
<template>
<ScrollArea
:class="[cn(props.class)]"
:class="[cn(props.class), computedShadowClasses]"
:on-scroll="handleScroll"
class="relative"
class="vben-scrollbar relative"
>
<div
v-if="shadow"
v-if="showShadowTop"
:class="{
'opacity-100': !isAtTop,
'border-border border-t': shadowBorder && !isAtTop,
@ -49,7 +90,7 @@ function handleScroll(event: Event) {
></div>
<slot></slot>
<div
v-if="shadow"
v-if="showShadowBottom"
:class="{
'opacity-100': !isAtTop && !isAtBottom,
'border-border border-b': shadowBorder && !isAtTop && !isAtBottom,
@ -65,6 +106,31 @@ function handleScroll(event: Event) {
</template>
<style scoped>
.vben-scrollbar {
&:not(.shadow-both).shadow-left {
mask-image: linear-gradient(90deg, transparent, #000 16px);
}
&:not(.shadow-both).shadow-right {
mask-image: linear-gradient(
90deg,
#000 0%,
#000 calc(100% - 16px),
transparent
);
}
&.shadow-both {
mask-image: linear-gradient(
90deg,
transparent,
#000 16px,
#000 calc(100% - 16px),
transparent 100%
);
}
}
.scrollbar-top-shadow {
background: linear-gradient(
to bottom,

View File

@ -41,6 +41,7 @@
"@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/typings": "workspace:*",
"vue": "^3.4.38"
"@vueuse/core": "^11.0.0",
"vue": "^3.4.37"
}
}

View File

@ -3,10 +3,10 @@ import type { TabDefinition } from '@vben-core/typings';
import type { TabConfig, TabsProps } from '../../types';
import { computed, ref, watch } from 'vue';
import { computed, ref } from 'vue';
import { MdiPin, X } from '@vben-core/icons';
import { VbenContextMenu, VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
interface Props extends TabsProps {}
@ -20,17 +20,17 @@ const props = withDefaults(defineProps<Props>(), {
contentClass: 'vben-tabs-content',
contextMenus: () => [],
gap: 7,
maxWidth: 150,
minWidth: 80,
tabs: () => [],
});
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
const emit = defineEmits<{
close: [string];
unpin: [TabDefinition];
}>();
const active = defineModel<string>('active');
const contentRef = ref();
const tabRef = ref();
const tabWidth = ref<number>(props.maxWidth);
const style = computed(() => {
const { gap } = props;
@ -53,51 +53,25 @@ const tabsView = computed((): TabConfig[] => {
};
});
});
watch(active, () => {
scrollIntoView();
});
function scrollIntoView() {
setTimeout(() => {
const element = document.querySelector(`.tabs-chrome__item.is-active`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
}
</script>
<template>
<div :style="style" class="tabs-chrome size-full flex-1 overflow-hidden pt-1">
<VbenScrollbar
id="tabs-scrollbar"
class="tabs-chrome__scrollbar h-full"
horizontal
scroll-bar-class="z-10 hidden"
>
<!-- footer -> 4px -->
<div
ref="contentRef"
:class="contentClass"
class="relative !flex h-full w-max"
:style="style"
class="tabs-chrome !flex h-full w-max pr-6"
>
<TransitionGroup name="slide-left">
<div
v-for="(tab, i) in tabsView"
:key="tab.key"
ref="tabRef"
:class="[
{ 'is-active': tab.key === active, dragable: !tab.affixTab },
]"
:class="[{ 'is-active': tab.key === active, dragable: !tab.affixTab }]"
:data-active-tab="active"
:data-index="i"
:style="{
width: `${tabWidth}px`,
left: `${(tabWidth - gap * 2) * i}px`,
}"
class="tabs-chrome__item group absolute flex h-full select-none items-center transition-all"
class="tabs-chrome__item draggable group relative -mr-3 flex h-full select-none items-center"
data-tab-item="true"
@click="active = tab.key"
>
<VbenContextMenu
@ -106,18 +80,18 @@ function scrollIntoView() {
:modal="false"
item-class="pr-6"
>
<div class="size-full">
<div class="relative size-full px-1">
<!-- divider -->
<div
v-if="i !== 0 && tab.key !== active"
class="tabs-chrome__divider bg-foreground/60 absolute left-[var(--gap)] top-1/2 z-0 h-4 w-[1px] translate-y-[-50%] transition-all"
class="tabs-chrome__divider bg-foreground/50 absolute left-[var(--gap)] top-1/2 z-0 h-4 w-[1px] translate-y-[-50%] transition-all"
></div>
<!-- background -->
<div
class="tabs-chrome__background absolute z-[1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
class="tabs-chrome__background absolute z-[-1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
>
<div
class="tabs-chrome__background-content group-[.is-active]:bg-primary/15 dark:group-[.is-active]:bg-accent h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150"
class="tabs-chrome__background-content group-[.is-active]:bg-heavy dark:group-[.is-active]:bg-accent h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150"
></div>
<svg
class="tabs-chrome__background-before group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 left-[-1px] fill-transparent transition-all duration-150"
@ -137,37 +111,33 @@ function scrollIntoView() {
<!-- extra -->
<div
class="tabs-chrome__extra absolute right-[calc(var(--gap)*1.5)] top-1/2 z-[3] size-4 translate-y-[-50%]"
class="tabs-chrome__extra absolute right-[var(--gap)] top-1/2 z-[3] size-4 translate-y-[-50%]"
>
<!-- close-icon -->
<X
v-show="
!tab.affixTab && tabsView.length > 1 && tab.closable
"
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary mt-[2px] size-3 cursor-pointer rounded-full transition-all"
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('close', tab.key)"
/>
<MdiPin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
class="hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('unpin', tab)"
/>
</div>
<!-- tab-item-main -->
<div
class="tabs-chrome__item-main group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground text-accent-foreground absolute left-0 right-0 z-[2] mx-[calc(var(--gap)*2)] my-0 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-4 duration-150 group-hover:pr-3"
class="tabs-chrome__item-main group-[.is-active]:text-accent-foreground dark:group-[.is-active]:text-accent-foreground text-accent-foreground z-[2] mx-[calc(var(--gap)*2)] my-0 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pl-2 pr-4 duration-150"
>
<VbenIcon
v-if="showIcon"
:icon="tab.icon"
class="ml-[var(--gap)] flex size-4 items-center overflow-hidden"
class="mr-1 flex size-4 items-center overflow-hidden"
fallback
/>
<span
class="tabs-chrome__label ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap text-sm"
>
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
{{ tab.title }}
</span>
</div>
@ -176,25 +146,25 @@ function scrollIntoView() {
</div>
</TransitionGroup>
</div>
<!-- footer -->
<!-- <div class="bg-background h-1"></div> -->
</VbenScrollbar>
</div>
</template>
<style scoped>
.tabs-chrome {
.dragging {
.tabs-chrome__item-main {
/* .dragging { */
/* .tabs-chrome__item-main {
@apply pr-0;
}
} */
.tabs-chrome__extra {
/* .tabs-chrome__extra {
@apply hidden;
}
}
} */
/* } */
&__item:not(.dragging) {
@apply cursor-pointer;
&__item {
&:hover:not(.is-active) {
& + .tabs-chrome__item {
.tabs-chrome__divider {
@ -207,13 +177,10 @@ function scrollIntoView() {
}
.tabs-chrome__background {
&-content {
@apply bg-accent mx-1 rounded-md pb-2;
}
@apply pb-[2px];
&-before,
&-after {
@apply fill-primary/0;
&-content {
@apply bg-accent-hover mx-[2px] rounded-md;
}
}
}
@ -226,30 +193,7 @@ function scrollIntoView() {
@apply opacity-0 !important;
}
}
.tabs-chrome__background {
@apply opacity-100;
/* &-content {
@apply bg-accent;
}
&-before,
&-after {
@apply fill-heavy;
} */
}
}
}
&__scrollbar,
&__label {
mask-image: linear-gradient(
90deg,
#000 0%,
#000 calc(100% - 16px),
transparent
);
}
}
</style>

View File

@ -3,10 +3,10 @@ import type { TabDefinition } from '@vben-core/typings';
import type { TabConfig, TabsProps } from '../../types';
import { computed, watch } from 'vue';
import { computed } from 'vue';
import { MdiPin, X } from '@vben-core/icons';
import { VbenContextMenu, VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
interface Props extends TabsProps {}
@ -21,7 +21,10 @@ const props = withDefaults(defineProps<Props>(), {
tabs: () => [],
});
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
const emit = defineEmits<{
close: [string];
unpin: [TabDefinition];
}>();
const active = defineModel<string>('active');
const typeWithClass = computed(() => {
@ -55,33 +58,12 @@ const tabsView = computed((): TabConfig[] => {
};
});
});
watch(active, () => {
scrollIntoView();
});
function scrollIntoView() {
setTimeout(() => {
const element = document.querySelector(`.tabs-chrome__item.is-active`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
}
</script>
<template>
<div class="size-full flex-1 overflow-hidden">
<VbenScrollbar
id="tabs-scrollbar"
class="tabs-scrollbar h-full"
horizontal
scroll-bar-class="z-10 hidden"
>
<div
:class="contentClass"
class="relative !flex h-full w-max items-center"
class="relative !flex h-full w-max items-center pr-6"
>
<TransitionGroup name="slide-left">
<div
@ -95,7 +77,8 @@ function scrollIntoView() {
typeWithClass.content,
]"
:data-index="i"
class="tabs-chrome__item [&:not(.is-active)]:hover:bg-accent group relative flex cursor-pointer select-none transition-all duration-300"
class="tab-item [&:not(.is-active)]:hover:bg-accent group relative flex cursor-pointer select-none"
data-tab-item="true"
@click="active = tab.key"
>
<VbenContextMenu
@ -111,9 +94,7 @@ function scrollIntoView() {
>
<!-- close-icon -->
<X
v-show="
!tab.affixTab && tabsView.length > 1 && tab.closable
"
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary size-3 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('close', tab.key)"
/>
@ -135,9 +116,7 @@ function scrollIntoView() {
fallback
/>
<span
class="flex-1 overflow-hidden whitespace-nowrap text-sm"
>
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
{{ tab.title }}
</span>
</div>
@ -146,17 +125,4 @@ function scrollIntoView() {
</div>
</TransitionGroup>
</div>
</VbenScrollbar>
</div>
</template>
<style scoped>
.tabs-scrollbar {
mask-image: linear-gradient(
90deg,
#000 0%,
#000 calc(100% - 16px),
transparent
);
}
</style>

View File

@ -1,15 +1,12 @@
<script setup lang="ts">
import type { Sortable } from '@vben-core/composables';
import type { TabDefinition } from '@vben-core/typings';
import type { TabsEmits, TabsProps } from './types';
import type { TabsProps } from './types';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useForwardPropsEmits, useSortable } from '@vben-core/composables';
import { useForwardPropsEmits } from '@vben-core/composables';
import { ChevronLeft, ChevronRight } from '@vben-core/icons';
import { VbenScrollbar } from '@vben-core/shadcn-ui';
import { Tabs, TabsChrome } from './components';
import { useTabsDrag } from './use-tabs-drag';
import { useTabsViewScroll } from './use-tabs-view-scroll';
interface Props extends TabsProps {}
@ -24,136 +21,69 @@ const props = withDefaults(defineProps<Props>(), {
styleType: 'chrome',
});
const emit = defineEmits<{
close: [string];
sortTabs: [number, number];
unpin: [TabDefinition];
}>();
const emit = defineEmits<TabsEmits>();
const forward = useForwardPropsEmits(props, emit);
const { initScrollbar, scrollDirection } = useTabsViewScroll();
const {
handleScrollAt,
scrollbarRef,
scrollDirection,
scrollIsAtLeft,
scrollIsAtRight,
showScrollButton,
} = useTabsViewScroll(props);
const sortableInstance = ref<null | Sortable>(null);
// domtab
function findParentElement(element: HTMLElement) {
const parentCls = 'group';
return element.classList.contains(parentCls)
? element
: element.closest(`.${parentCls}`);
}
async function initTabsSortable() {
await nextTick();
const { contentClass } = props;
const el = document.querySelectorAll(`.${contentClass}`)?.[0] as HTMLElement;
const resetElState = () => {
el.style.cursor = 'default';
el.classList.remove('dragging');
};
const { initializeSortable } = useSortable(el, {
filter: (_evt, target: HTMLElement) => {
const parent = findParentElement(target);
const dragable = parent?.classList.contains('dragable');
return !dragable || !props.dragable;
},
onEnd(evt) {
const { newIndex, oldIndex } = evt;
// const fromElement = evt.item;
const { srcElement } = (evt as any).originalEvent;
if (!srcElement) {
resetElState();
return;
}
const srcParent = findParentElement(srcElement);
if (!srcParent) {
resetElState();
return;
}
if (!srcParent.classList.contains('dragable')) {
resetElState();
return;
}
if (
oldIndex !== undefined &&
newIndex !== undefined &&
!Number.isNaN(oldIndex) &&
!Number.isNaN(newIndex) &&
oldIndex !== newIndex
) {
emit('sortTabs', oldIndex, newIndex);
}
resetElState();
},
onMove(evt) {
const parent = findParentElement(evt.related);
return parent?.classList.contains('dragable') && props.dragable;
},
onStart: () => {
el.style.cursor = 'grabbing';
el.classList.add('dragging');
},
});
sortableInstance.value = await initializeSortable();
}
async function init() {
await nextTick();
initTabsSortable();
initScrollbar();
}
onMounted(() => {
init();
});
watch(
() => props.styleType,
() => {
sortableInstance.value?.destroy();
init();
},
);
onUnmounted(() => {
sortableInstance.value?.destroy();
});
useTabsDrag(props, emit);
</script>
<template>
<div
:class="{
'overflow-hidden': styleType !== 'chrome',
}"
class="flex h-full flex-1"
>
<div class="flex h-full flex-1 overflow-hidden">
<!-- 左侧滚动按钮 -->
<span
class="hover:bg-muted text-muted-foreground cursor-pointer border-r px-2"
v-show="showScrollButton"
:class="{
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtLeft,
'pointer-events-none opacity-30': scrollIsAtLeft,
}"
class="border-r px-2"
@click="scrollDirection('left')"
>
<ChevronLeft class="size-4 h-full" />
</span>
<div
:class="{
'pt-[3px]': styleType === 'chrome',
}"
class="size-full flex-1 overflow-hidden"
>
<VbenScrollbar
ref="scrollbarRef"
class="h-full"
horizontal
scroll-bar-class="z-10 hidden"
shadow
shadow-left
shadow-right
@scroll-at="handleScrollAt"
>
<TabsChrome
v-if="styleType === 'chrome'"
v-bind="{ ...forward, ...$attrs, ...$props }"
/>
<Tabs v-else v-bind="{ ...forward, ...$attrs, ...$props }" />
</VbenScrollbar>
</div>
<!-- 左侧滚动按钮 -->
<span
v-show="showScrollButton"
:class="{
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtRight,
'pointer-events-none opacity-30': scrollIsAtRight,
}"
class="hover:bg-muted text-muted-foreground cursor-pointer border-l px-2"
@click="scrollDirection('right')"
>

View File

@ -1,7 +1,14 @@
import type { IContextMenuItem } from '@vben-core/shadcn-ui';
import type { TabDefinition, TabsStyleType } from '@vben-core/typings';
interface TabsProps {
export type TabsEmits = {
close: [string];
sortTabs: [number, number];
unpin: [TabDefinition];
};
export interface TabsProps {
active?: string;
/**
* @zh_CN content class
* @default tabs-chrome
@ -48,12 +55,10 @@ interface TabsProps {
tabs?: TabDefinition[];
}
interface TabConfig extends TabDefinition {
export interface TabConfig extends TabDefinition {
affixTab: boolean;
closable: boolean;
icon: string;
key: string;
title: string;
}
export type { TabConfig, TabsProps };

View File

@ -0,0 +1,110 @@
import type { EmitType } from '@vben-core/typings';
import type { TabsProps } from './types';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { type Sortable, useSortable } from '@vben-core/composables';
// 可能会找到拖拽的子元素这里需要确保拖拽的dom时tab元素
function findParentElement(element: HTMLElement) {
const parentCls = 'group';
return element.classList.contains(parentCls)
? element
: element.closest(`.${parentCls}`);
}
export function useTabsDrag(props: TabsProps, emit: EmitType) {
const sortableInstance = ref<null | Sortable>(null);
async function initTabsSortable() {
await nextTick();
const el = document.querySelectorAll(
`.${props.contentClass}`,
)?.[0] as HTMLElement;
if (!el) {
console.warn('Element not found for sortable initialization');
return;
}
const resetElState = async () => {
el.style.cursor = 'default';
el.classList.remove('dragging');
el.querySelector('.draggable')?.classList.remove('dragging');
};
const { initializeSortable } = useSortable(el, {
filter: (_evt, target: HTMLElement) => {
const parent = findParentElement(target);
const dragable = parent?.classList.contains('dragable');
return !dragable || !props.dragable;
},
onEnd(evt) {
const { newIndex, oldIndex } = evt;
// const fromElement = evt.item;
const { srcElement } = (evt as any).originalEvent;
if (!srcElement) {
resetElState();
return;
}
const srcParent = findParentElement(srcElement);
if (!srcParent) {
resetElState();
return;
}
if (!srcParent.classList.contains('dragable')) {
resetElState();
return;
}
if (
oldIndex !== undefined &&
newIndex !== undefined &&
!Number.isNaN(oldIndex) &&
!Number.isNaN(newIndex) &&
oldIndex !== newIndex
) {
emit('sortTabs', oldIndex, newIndex);
}
resetElState();
},
onMove(evt) {
const parent = findParentElement(evt.related);
return parent?.classList.contains('dragable') && props.dragable;
},
onStart: () => {
el.style.cursor = 'grabbing';
el.querySelector('.draggable')?.classList.add('dragging');
// el.classList.add('dragging');
},
});
sortableInstance.value = await initializeSortable();
}
async function init() {
await nextTick();
initTabsSortable();
}
onMounted(init);
watch(
() => props.styleType,
() => {
sortableInstance.value?.destroy();
init();
},
);
onUnmounted(() => {
sortableInstance.value?.destroy();
});
}

View File

@ -1,15 +1,28 @@
import { nextTick, ref } from 'vue';
import type { TabsProps } from './types';
type El = Element | null | undefined;
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
export function useTabsViewScroll(scrollDistance: number = 150) {
const scrollbarEl = ref<El>(null);
const scrollViewportEl = ref<El>(null);
import { VbenScrollbar } from '@vben-core/shadcn-ui';
import { useDebounceFn } from '@vueuse/core';
type DomElement = Element | null | undefined;
export function useTabsViewScroll(props: TabsProps) {
let resizeObserver: null | ResizeObserver = null;
let mutationObserver: MutationObserver | null = null;
let tabItemCount = 0;
const scrollbarRef = ref<InstanceType<typeof VbenScrollbar> | null>(null);
const scrollViewportEl = ref<DomElement>(null);
const showScrollButton = ref(false);
const scrollIsAtLeft = ref(true);
const scrollIsAtRight = ref(false);
function getScrollClientWidth() {
if (!scrollbarEl.value || !scrollViewportEl.value) return {};
const scrollbarEl = scrollbarRef.value?.$el;
if (!scrollbarEl || !scrollViewportEl.value) return {};
const scrollbarWidth = scrollbarEl.value.clientWidth;
const scrollbarWidth = scrollbarEl.clientWidth;
const scrollViewWidth = scrollViewportEl.value.clientWidth;
return {
@ -20,7 +33,7 @@ export function useTabsViewScroll(scrollDistance: number = 150) {
function scrollDirection(
direction: 'left' | 'right',
distance: number = scrollDistance,
distance: number = 150,
) {
const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();
@ -39,21 +52,142 @@ export function useTabsViewScroll(scrollDistance: number = 150) {
async function initScrollbar() {
await nextTick();
const barEl = document.querySelector('#tabs-scrollbar');
const viewportEl = barEl?.querySelector(
const scrollbarEl = scrollbarRef.value?.$el;
if (!scrollbarEl) {
return;
}
const viewportEl = scrollbarEl?.querySelector(
'div[data-radix-scroll-area-viewport]',
);
scrollbarEl.value = barEl;
scrollViewportEl.value = viewportEl;
calcShowScrollbarButton();
const activeItem = viewportEl?.querySelector('.is-active');
activeItem?.scrollIntoView({ behavior: 'smooth', block: 'start' });
await nextTick();
scrollToActiveIntoView();
// 监听大小变化
resizeObserver?.disconnect();
resizeObserver = new ResizeObserver(
useDebounceFn((_entries: ResizeObserverEntry[]) => {
calcShowScrollbarButton();
}, 100),
);
resizeObserver.observe(viewportEl);
tabItemCount = props.tabs?.length || 0;
mutationObserver?.disconnect();
// 使用 MutationObserver 仅监听子节点数量变化
mutationObserver = new MutationObserver(() => {
const count = viewportEl.querySelectorAll(
`div[data-tab-item="true"]`,
).length;
if (count > tabItemCount) {
scrollToActiveIntoView();
}
if (count !== tabItemCount) {
calcShowScrollbarButton();
tabItemCount = count;
}
});
// 配置为仅监听子节点的添加和移除
mutationObserver.observe(viewportEl, {
attributes: false,
childList: true,
subtree: true,
});
}
async function scrollToActiveIntoView() {
if (!scrollViewportEl.value) {
return;
}
await nextTick();
const viewportEl = scrollViewportEl.value;
const { scrollbarWidth } = getScrollClientWidth();
const { scrollWidth } = viewportEl;
if (scrollbarWidth >= scrollWidth) {
return;
}
requestAnimationFrame(() => {
const activeItem = viewportEl?.querySelector('.is-active');
activeItem?.scrollIntoView({ behavior: 'smooth', inline: 'start' });
});
}
/**
* tabs
*/
async function calcShowScrollbarButton() {
if (!scrollViewportEl.value) {
return;
}
const { scrollbarWidth } = getScrollClientWidth();
showScrollButton.value =
scrollViewportEl.value.scrollWidth > scrollbarWidth;
}
const handleScrollAt = useDebounceFn(({ left, right }) => {
scrollIsAtLeft.value = left;
scrollIsAtRight.value = right;
}, 100);
watch(
() => props.active,
async () => {
// 200为了等待 tab 切换动画完成
// setTimeout(() => {
scrollToActiveIntoView();
// }, 300);
},
{
flush: 'post',
},
);
// watch(
// () => props.tabs?.length,
// async () => {
// await nextTick();
// calcShowScrollbarButton();
// },
// {
// flush: 'post',
// },
// );
watch(
() => props.styleType,
() => {
initScrollbar();
},
);
onMounted(initScrollbar);
onUnmounted(() => {
resizeObserver?.disconnect();
mutationObserver?.disconnect();
resizeObserver = null;
mutationObserver = null;
});
return {
handleScrollAt,
initScrollbar,
scrollbarRef,
scrollDirection,
scrollIsAtLeft,
scrollIsAtRight,
showScrollButton,
};
}

View File

@ -24,6 +24,6 @@
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"vue": "^3.4.38"
"vue": "^3.4.37"
}
}

View File

@ -21,8 +21,8 @@
},
"dependencies": {
"@vben/preferences": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"echarts": "^5.5.1",
"vue": "^3.4.38"
"vue": "^3.4.37"
}
}

View File

@ -26,9 +26,9 @@
"@vben/icons": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/types": "workspace:*",
"@vueuse/integrations": "^10.11.1",
"@vueuse/integrations": "^11.0.0",
"qrcode": "^1.5.4",
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
},
"devDependencies": {

View File

@ -25,7 +25,7 @@
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-router": "^4.4.3",
"watermark-js-plus": "^1.5.3"
}

View File

@ -32,8 +32,8 @@
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1",
"vue": "^3.4.38",
"@vueuse/core": "^11.0.0",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
}
}

View File

@ -21,7 +21,7 @@
},
"dependencies": {
"@intlify/core-base": "^9.13.1",
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-i18n": "^9.13.1"
}
}

View File

@ -24,7 +24,7 @@
"@vben-core/typings": "workspace:*",
"pinia": "2.2.2",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
}
}

View File

@ -124,10 +124,21 @@ export const useTabbarStore = defineStore('core-tabbar', {
} else {
// 页面已经存在,不重复添加选项卡,只更新选项卡参数
const currentTab = toRaw(this.tabs)[tabIndex];
const mergedTab = { ...currentTab, ...tab };
if (currentTab && Reflect.has(currentTab.meta, 'affixTab')) {
mergedTab.meta.affixTab = currentTab.meta.affixTab;
const mergedTab = {
...currentTab,
...tab,
meta: { ...currentTab?.meta, ...tab.meta },
};
if (currentTab) {
const curMeta = currentTab.meta;
if (Reflect.has(curMeta, 'affixTab')) {
mergedTab.meta.affixTab = curMeta.affixTab;
}
if (Reflect.has(curMeta, 'newTabTitle')) {
mergedTab.meta.newTabTitle = curMeta.newTabTitle;
}
}
this.tabs.splice(tabIndex, 1, mergedTab);
}
this.updateCacheTab();

View File

@ -21,7 +21,7 @@
},
"dependencies": {
"@vben-core/typings": "workspace:*",
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
}
}

View File

@ -40,11 +40,11 @@
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.1",
"@vueuse/core": "^11.0.0",
"ant-design-vue": "^4.2.3",
"dayjs": "^1.11.12",
"pinia": "2.2.2",
"vue": "^3.4.38",
"vue": "^3.4.37",
"vue-router": "^4.4.3"
}
}

View File

@ -161,7 +161,7 @@ const routes: RouteRecordRaw[] = [
import(
'#/views/demos/features/hide-menu-children/children.vue'
),
meta: { title: 'HideChildrenInMenuChildrenDemo' },
meta: { title: $t('page.demos.features.hideChildrenInMenu') },
},
],
},

View File

@ -165,8 +165,8 @@ importers:
specifier: workspace:*
version: link:../../packages/utils
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
ant-design-vue:
specifier: ^4.2.3
version: 4.2.3(vue@3.4.38(typescript@5.5.4))
@ -228,8 +228,8 @@ importers:
specifier: workspace:*
version: link:../../packages/utils
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
dayjs:
specifier: ^1.11.12
version: 1.11.12
@ -295,8 +295,8 @@ importers:
specifier: workspace:*
version: link:../../packages/utils
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
naive-ui:
specifier: ^2.39.0
version: 2.39.0(vue@3.4.38(typescript@5.5.4))
@ -660,8 +660,8 @@ importers:
specifier: ^4.1.2
version: 4.1.2(vue@3.4.38(typescript@5.5.4))
lucide-vue-next:
specifier: ^0.427.0
version: 0.427.0(vue@3.4.38(typescript@5.5.4))
specifier: ^0.428.0
version: 0.428.0(vue@3.4.38(typescript@5.5.4))
vue:
specifier: 3.4.38
version: 3.4.38(typescript@5.5.4)
@ -672,7 +672,7 @@ importers:
specifier: 4.1.0
version: 4.1.0
'@vue/shared':
specifier: ^3.4.38
specifier: ^3.4.37
version: 3.4.38
clsx:
specifier: 2.1.1
@ -715,8 +715,8 @@ importers:
specifier: workspace:*
version: link:../base/shared
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
radix-vue:
specifier: ^1.9.4
version: 1.9.4(vue@3.4.38(typescript@5.5.4))
@ -740,8 +740,8 @@ importers:
specifier: workspace:*
version: link:../base/typings
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue:
specifier: 3.4.38
version: 3.4.38(typescript@5.5.4)
@ -761,8 +761,8 @@ importers:
specifier: workspace:*
version: link:../../base/typings
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue:
specifier: 3.4.38
version: 3.4.38(typescript@5.5.4)
@ -785,8 +785,8 @@ importers:
specifier: workspace:*
version: link:../../base/typings
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue:
specifier: 3.4.38
version: 3.4.38(typescript@5.5.4)
@ -806,14 +806,14 @@ importers:
specifier: workspace:*
version: link:../../base/typings
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
lucide-vue-next:
specifier: ^0.427.0
version: 0.427.0(vue@3.4.38(typescript@5.5.4))
specifier: ^0.428.0
version: 0.428.0(vue@3.4.38(typescript@5.5.4))
radix-vue:
specifier: ^1.9.4
version: 1.9.4(vue@3.4.38(typescript@5.5.4))
@ -835,6 +835,9 @@ importers:
'@vben-core/typings':
specifier: workspace:*
version: link:../../base/typings
'@vueuse/core':
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue:
specifier: 3.4.38
version: 3.4.38(typescript@5.5.4)
@ -869,8 +872,8 @@ importers:
specifier: workspace:*
version: link:../../preferences
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
echarts:
specifier: ^5.5.1
version: 5.5.1
@ -899,8 +902,8 @@ importers:
specifier: workspace:*
version: link:../../types
'@vueuse/integrations':
specifier: ^10.11.1
version: 10.11.1(async-validator@4.2.5)(axios@1.7.4)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(async-validator@4.2.5)(axios@1.7.4)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))
qrcode:
specifier: ^1.5.4
version: 1.5.4
@ -981,8 +984,8 @@ importers:
specifier: workspace:*
version: link:../../utils
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
vue:
specifier: 3.4.38
version: 3.4.38(typescript@5.5.4)
@ -1129,8 +1132,8 @@ importers:
specifier: workspace:*
version: link:../packages/utils
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.38(typescript@5.5.4))
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.5.4))
ant-design-vue:
specifier: ^4.2.3
version: 4.2.3(vue@3.4.38(typescript@5.5.4))
@ -4179,6 +4182,9 @@ packages:
'@vueuse/core@10.11.1':
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
'@vueuse/core@11.0.0':
resolution: {integrity: sha512-shibzNGjmRjZucEm97B8V0NO5J3vPHMCE/mltxQ3vHezbDoFQBMtK11XsfwfPionxSbo+buqPmsCljtYuXIBpw==}
'@vueuse/core@9.13.0':
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
@ -4223,21 +4229,21 @@ packages:
universal-cookie:
optional: true
'@vueuse/integrations@10.11.1':
resolution: {integrity: sha512-Y5hCGBguN+vuVYTZmdd/IMXLOdfS60zAmDmFYc4BKBcMUPZH1n4tdyDECCPjXm0bNT3ZRUy1xzTLGaUje8Xyaw==}
'@vueuse/integrations@11.0.0':
resolution: {integrity: sha512-B95nBX4B2q2ZETBDldrKARM/fYXBHfwdo44UbHBq4bUTi25lrlc8MwAZGqEoRvdV4ND9T6O1Rb9e4kaCJFXnqw==}
peerDependencies:
async-validator: ^4
axios: ^1
change-case: ^4
drauu: ^0.3
change-case: ^5
drauu: ^0.4
focus-trap: ^7
fuse.js: ^6
fuse.js: ^7
idb-keyval: ^6
jwt-decode: ^3
jwt-decode: ^4
nprogress: ^0.2
qrcode: ^1.5
sortablejs: ^1
universal-cookie: ^6
universal-cookie: ^7
peerDependenciesMeta:
async-validator:
optional: true
@ -4270,6 +4276,9 @@ packages:
'@vueuse/metadata@10.11.1':
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
'@vueuse/metadata@11.0.0':
resolution: {integrity: sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==}
'@vueuse/metadata@9.13.0':
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
@ -4279,6 +4288,9 @@ packages:
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
'@vueuse/shared@11.0.0':
resolution: {integrity: sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==}
'@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
@ -6916,8 +6928,8 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-vue-next@0.427.0:
resolution: {integrity: sha512-zI1FhbfQ3Wl0SgPKnOWhTDC6yAC5TTjSC9FSZ61ULg3U36e+GVK+RT1qfkU9Q5BjeBuwmsHWKsXKptKMjUAwFA==}
lucide-vue-next@0.428.0:
resolution: {integrity: sha512-of9GJGus9VKGIUOp3yQ0uQtNv+8MRLaso8H4OiDzI6+T7TeMRXTzqVOLhnyg9fdXUnYuwE9Xm1zD1nfQ7oFPmg==}
peerDependencies:
vue: 3.4.38
@ -13196,6 +13208,16 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/core@11.0.0(vue@3.4.38(typescript@5.5.4))':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 11.0.0
'@vueuse/shared': 11.0.0(vue@3.4.38(typescript@5.5.4))
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/core@9.13.0(vue@3.4.38(typescript@5.5.4))':
dependencies:
'@types/web-bluetooth': 0.0.16
@ -13222,10 +13244,10 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/integrations@10.11.1(async-validator@4.2.5)(axios@1.7.4)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))':
'@vueuse/integrations@11.0.0(async-validator@4.2.5)(axios@1.7.4)(focus-trap@7.5.4)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.2)(vue@3.4.38(typescript@5.5.4))':
dependencies:
'@vueuse/core': 10.11.1(vue@3.4.38(typescript@5.5.4))
'@vueuse/shared': 10.11.1(vue@3.4.38(typescript@5.5.4))
'@vueuse/core': 11.0.0(vue@3.4.38(typescript@5.5.4))
'@vueuse/shared': 11.0.0(vue@3.4.38(typescript@5.5.4))
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
optionalDependencies:
async-validator: 4.2.5
@ -13242,6 +13264,8 @@ snapshots:
'@vueuse/metadata@10.11.1': {}
'@vueuse/metadata@11.0.0': {}
'@vueuse/metadata@9.13.0': {}
'@vueuse/shared@10.11.0(vue@3.4.38(typescript@5.5.4))':
@ -13258,6 +13282,13 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/shared@11.0.0(vue@3.4.38(typescript@5.5.4))':
dependencies:
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/shared@9.13.0(vue@3.4.38(typescript@5.5.4))':
dependencies:
vue-demi: 0.14.10(vue@3.4.38(typescript@5.5.4))
@ -16250,7 +16281,7 @@ snapshots:
dependencies:
yallist: 4.0.0
lucide-vue-next@0.427.0(vue@3.4.38(typescript@5.5.4)):
lucide-vue-next@0.428.0(vue@3.4.38(typescript@5.5.4)):
dependencies:
vue: 3.4.38(typescript@5.5.4)