35 changed files with 662 additions and 518 deletions
@ -0,0 +1,7 @@ |
|||
import { defineBuildConfig } from 'unbuild'; |
|||
|
|||
export default defineBuildConfig({ |
|||
clean: true, |
|||
declaration: true, |
|||
entries: ['src/index'], |
|||
}); |
|||
@ -0,0 +1,45 @@ |
|||
{ |
|||
"name": "@vben-core/hooks", |
|||
"version": "5.0.0", |
|||
"homepage": "https://github.com/vbenjs/vue-vben-admin", |
|||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git", |
|||
"directory": "packages/@vben-core/shared/hooks" |
|||
}, |
|||
"license": "MIT", |
|||
"type": "module", |
|||
"scripts": { |
|||
"build": "pnpm unbuild", |
|||
"stub": "pnpm unbuild --stub" |
|||
}, |
|||
"files": [ |
|||
"dist" |
|||
], |
|||
"sideEffects": false, |
|||
"main": "./dist/index.mjs", |
|||
"module": "./dist/index.mjs", |
|||
"exports": { |
|||
".": { |
|||
"types": "./src/index.ts", |
|||
"development": "./src/index.ts", |
|||
"default": "./dist/index.mjs" |
|||
} |
|||
}, |
|||
"publishConfig": { |
|||
"exports": { |
|||
".": { |
|||
"types": "./dist/index.d.ts", |
|||
"default": "./dist/index.mjs" |
|||
} |
|||
} |
|||
}, |
|||
"dependencies": { |
|||
"sortablejs": "^1.15.2", |
|||
"vue": "^3.4.31" |
|||
}, |
|||
"devDependencies": { |
|||
"@types/sortablejs": "^1.15.8" |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './use-sortable'; |
|||
@ -0,0 +1,46 @@ |
|||
import type { SortableOptions } from 'sortablejs'; |
|||
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
|||
|
|||
import { useSortable } from './use-sortable'; |
|||
|
|||
describe('useSortable', () => { |
|||
beforeEach(() => { |
|||
vi.mock('sortablejs', () => ({ |
|||
default: { |
|||
create: vi.fn(), |
|||
}, |
|||
})); |
|||
}); |
|||
it('should call Sortable.create with the correct options', async () => { |
|||
// Create a mock element
|
|||
const mockElement = document.createElement('div') as HTMLDivElement; |
|||
|
|||
// Define custom options
|
|||
const customOptions: SortableOptions = { |
|||
group: 'test-group', |
|||
sort: false, |
|||
}; |
|||
|
|||
// Use the useSortable function
|
|||
const { initializeSortable } = useSortable(mockElement, customOptions); |
|||
|
|||
// Initialize sortable
|
|||
await initializeSortable(); |
|||
|
|||
// Import sortablejs to access the mocked create function
|
|||
const Sortable = await import('sortablejs'); |
|||
|
|||
// Verify that Sortable.create was called with the correct parameters
|
|||
expect(Sortable.default.create).toHaveBeenCalledTimes(1); |
|||
expect(Sortable.default.create).toHaveBeenCalledWith( |
|||
mockElement, |
|||
expect.objectContaining({ |
|||
animation: 100, |
|||
delay: 400, |
|||
delayOnTouchOnly: true, |
|||
...customOptions, |
|||
}), |
|||
); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,33 @@ |
|||
import type { SortableOptions } from 'sortablejs'; |
|||
|
|||
function useSortable<T extends HTMLElement>( |
|||
sortableContainer: T, |
|||
options: SortableOptions = {}, |
|||
) { |
|||
const initializeSortable = async () => { |
|||
const Sortable = await import( |
|||
// @ts-expect-error - This is a dynamic import
|
|||
'sortablejs/modular/sortable.complete.esm.js' |
|||
); |
|||
// const { AutoScroll } = await import(
|
|||
// // @ts-expect-error - This is a dynamic import
|
|||
// 'sortablejs/modular/sortable.core.esm.js'
|
|||
// );
|
|||
|
|||
// Sortable?.default?.mount?.(AutoScroll);
|
|||
|
|||
const sortable = Sortable?.default?.create?.(sortableContainer, { |
|||
animation: 100, |
|||
delay: 400, |
|||
delayOnTouchOnly: true, |
|||
...options, |
|||
}); |
|||
return sortable; |
|||
}; |
|||
|
|||
return { |
|||
initializeSortable, |
|||
}; |
|||
} |
|||
|
|||
export { useSortable }; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/library.json", |
|||
"include": ["src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
import { defineBuildConfig } from 'unbuild'; |
|||
|
|||
export default defineBuildConfig({ |
|||
clean: true, |
|||
declaration: true, |
|||
entries: [ |
|||
{ |
|||
builder: 'mkdist', |
|||
input: './src', |
|||
loaders: ['vue'], |
|||
pattern: ['**/*.vue'], |
|||
}, |
|||
{ |
|||
builder: 'mkdist', |
|||
format: 'esm', |
|||
input: './src', |
|||
loaders: ['js'], |
|||
pattern: ['**/*.ts'], |
|||
}, |
|||
], |
|||
}); |
|||
@ -1,193 +0,0 @@ |
|||
@import '@vben-core/design/bem'; |
|||
|
|||
@include b('chrome-tabs') { |
|||
--tabs-background: hsl(var(--background)); |
|||
--tabs-gap: 7px; |
|||
--tabs-divider: hsl(var(--border)); |
|||
--tabs-hover: hsl(var(--heavy)); |
|||
--tabs-active-background: hsl(var(--primary) / 100%); |
|||
--tabs-active: hsl(var(--primary-foreground)); |
|||
|
|||
background-color: var(--tabs-background); |
|||
} |
|||
|
|||
@include b('chrome-tab') { |
|||
color: hsl(var(--muted-foreground)); |
|||
|
|||
@include is('active') { |
|||
z-index: 2; |
|||
color: var(--tabs-active); |
|||
|
|||
.#{$namespace}-chrome-tab__extra:not(.is-pin) { |
|||
background-color: var(--tabs-active-background); |
|||
opacity: 1; |
|||
} |
|||
|
|||
.#{$namespace}-chrome-tab-background__divider { |
|||
display: none; |
|||
} |
|||
|
|||
.#{$namespace}-chrome-tab-background__content { |
|||
background-color: var(--tabs-active-background); |
|||
} |
|||
|
|||
.#{$namespace}-chrome-tab-background__before, |
|||
.#{$namespace}-chrome-tab-background__after { |
|||
fill: var(--tabs-active-background); |
|||
} |
|||
} |
|||
|
|||
@include e('content') { |
|||
position: absolute; |
|||
right: 0; |
|||
left: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
height: 100%; |
|||
padding-right: 10px; |
|||
margin: 0 calc(var(--tabs-gap) * 2); |
|||
overflow: hidden; |
|||
border-top-left-radius: 5px; |
|||
border-top-right-radius: 5px; |
|||
} |
|||
|
|||
@include e('extra') { |
|||
position: absolute; |
|||
top: 50%; |
|||
right: calc(var(--tabs-gap) * 2); |
|||
z-index: 1; |
|||
width: 14px; |
|||
height: 14px; |
|||
border-radius: 50%; |
|||
opacity: 0; |
|||
transition: 0.15s; |
|||
transform: translateY(-50%); |
|||
|
|||
// &:hover { |
|||
// background-color: hsl(var(--accent)); |
|||
// } |
|||
} |
|||
|
|||
@include e('extra-icon') { |
|||
flex-shrink: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
font-size: 12px; |
|||
border-radius: 50%; |
|||
transition: all 0.15s ease; |
|||
|
|||
&:hover { |
|||
color: hsl(var(--foreground)); |
|||
transform: scale(1.05); |
|||
} |
|||
} |
|||
|
|||
@include e('icon') { |
|||
display: flex; |
|||
align-items: center; |
|||
height: 16px; |
|||
margin-left: 3%; |
|||
overflow: hidden; |
|||
|
|||
img { |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
@include e('label') { |
|||
position: relative; |
|||
flex: 1; |
|||
margin-right: 8px; |
|||
margin-left: 5%; |
|||
overflow: hidden; |
|||
font-size: 14px; |
|||
white-space: nowrap; |
|||
mask-image: linear-gradient( |
|||
90deg, |
|||
#000 0%, |
|||
#000 calc(100% - 20px), |
|||
transparent |
|||
); |
|||
|
|||
// &.no-close { |
|||
// margin-right: 0; |
|||
// } |
|||
|
|||
// &.no-icon { |
|||
// margin-left: 0; |
|||
// } |
|||
} |
|||
|
|||
@include is('hidden-icon') { |
|||
margin-left: 0; |
|||
} |
|||
|
|||
&:hover { |
|||
.#{$namespace}-chrome-tab__extra.is-pin { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
&:not(.is-active):hover { |
|||
z-index: 1; |
|||
|
|||
.#{$namespace}-chrome-tab__extra { |
|||
opacity: 1; |
|||
} |
|||
|
|||
.#{$namespace}-chrome-tab-background__divider { |
|||
display: none; |
|||
} |
|||
|
|||
.#{$namespace}-chrome-tab-background__content { |
|||
background-color: var(--tabs-hover); |
|||
} |
|||
|
|||
.#{$namespace}-chrome-tab-background__before, |
|||
.#{$namespace}-chrome-tab-background__after { |
|||
fill: var(--tabs-hover); |
|||
} |
|||
} |
|||
|
|||
&:first-of-type { |
|||
.#{$namespace}-chrome-tab-background__divider::before { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@include b('chrome-tab-background') { |
|||
padding: 0 calc(var(--tabs-gap) + 0px); |
|||
|
|||
@include e('divider') { |
|||
width: calc(100% - 14px); |
|||
margin: 0 7px; |
|||
|
|||
&::before { |
|||
background-color: var(--tabs-divider); |
|||
} |
|||
|
|||
&::after { |
|||
left: calc(100% - 1px); |
|||
background-color: var(--tabs-divider); |
|||
} |
|||
} |
|||
|
|||
@include e('content') { |
|||
border-top-left-radius: 5px; |
|||
border-top-right-radius: 5px; |
|||
transition: background 0.15s ease; |
|||
} |
|||
|
|||
@include e('before') { |
|||
bottom: -1px; |
|||
left: -3px; |
|||
transition: 0.15s; |
|||
} |
|||
|
|||
@include e('after') { |
|||
right: -3px; |
|||
bottom: -1px; |
|||
transition: 0.15s; |
|||
} |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
<script setup lang="ts"> |
|||
import { useNamespace } from '@vben-core/toolkit'; |
|||
|
|||
defineOptions({ |
|||
name: 'ChromeTabBackground', |
|||
}); |
|||
|
|||
const { b, e } = useNamespace('chrome-tab-background'); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="b()" class="absolute size-full"> |
|||
<div |
|||
:class="e('divider')" |
|||
class="absolute left-0 h-full before:absolute before:right-[100%] before:top-[15%] before:h-[60%] before:w-[1px] before:content-[''] after:absolute after:top-[15%] after:h-[60%] after:w-[1px] after:content-['']" |
|||
></div> |
|||
<div :class="e('content')" class="h-full"></div> |
|||
<svg |
|||
:class="e('before')" |
|||
class="absolute fill-transparent" |
|||
height="10" |
|||
width="10" |
|||
> |
|||
<path d="M 0 10 A 10 10 0 0 0 10 0 L 10 10 Z" /> |
|||
</svg> |
|||
<svg |
|||
:class="e('after')" |
|||
class="absolute fill-transparent" |
|||
height="10" |
|||
width="10" |
|||
> |
|||
<path d="M 0 0 A 10 10 0 0 0 10 10 L 0 10 Z" /> |
|||
</svg> |
|||
</div> |
|||
</template> |
|||
@ -1,76 +0,0 @@ |
|||
<script setup lang="ts"> |
|||
import type { IContextMenuItem } from '@vben-core/shadcn-ui'; |
|||
import type { TabItem } from '@vben-core/typings'; |
|||
|
|||
import { IcRoundClose, MdiPin } from '@vben-core/iconify'; |
|||
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui'; |
|||
import { useNamespace } from '@vben-core/toolkit'; |
|||
|
|||
import TabBackground from './tab-background.vue'; |
|||
|
|||
interface Props { |
|||
affixTab?: boolean; |
|||
icon?: string; |
|||
menus: (data: any) => IContextMenuItem[]; |
|||
onlyOne?: boolean; |
|||
showIcon?: boolean; |
|||
tab: TabItem; |
|||
title: string; |
|||
} |
|||
|
|||
defineOptions({ |
|||
name: 'ChromeTab', |
|||
}); |
|||
|
|||
withDefaults(defineProps<Props>(), { |
|||
icon: '', |
|||
}); |
|||
const emit = defineEmits<{ close: []; unpinTab: [] }>(); |
|||
|
|||
const { b, e, is } = useNamespace('chrome-tab'); |
|||
|
|||
function handleClose() { |
|||
emit('close'); |
|||
} |
|||
function handleUnpinTab() { |
|||
emit('unpinTab'); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div |
|||
:class="[b()]" |
|||
class="absolute flex h-full cursor-pointer select-none items-center" |
|||
> |
|||
<VbenContextMenu |
|||
:handler-data="tab" |
|||
:menus="menus" |
|||
:modal="false" |
|||
item-class="pr-4" |
|||
> |
|||
<div class="h-full"> |
|||
<TabBackground /> |
|||
<div :class="e('content')" :title="title"> |
|||
<VbenIcon v-if="showIcon" :class="e('icon')" :icon="icon" fallback /> |
|||
<span :class="[e('label'), is('hidden-icon', !icon)]"> |
|||
{{ title }} |
|||
</span> |
|||
</div> |
|||
<div |
|||
v-show="!affixTab && !onlyOne" |
|||
:class="e('extra')" |
|||
@click.stop="handleClose" |
|||
> |
|||
<IcRoundClose :class="e('extra-icon')" /> |
|||
</div> |
|||
<div |
|||
v-show="affixTab && !onlyOne" |
|||
:class="[e('extra'), is('pin', true)]" |
|||
@click.stop="handleUnpinTab" |
|||
> |
|||
<MdiPin :class="e('extra-icon')" /> |
|||
</div> |
|||
</div> |
|||
</VbenContextMenu> |
|||
</div> |
|||
</template> |
|||
@ -1,114 +0,0 @@ |
|||
<script setup lang="ts"> |
|||
import type { TabItem } from '@vben-core/typings'; |
|||
|
|||
import type { TabsProps } from '../../types'; |
|||
|
|||
import { computed, nextTick, onMounted, ref, watch } from 'vue'; |
|||
|
|||
import { useNamespace } from '@vben-core/toolkit'; |
|||
|
|||
import Tab from './tab.vue'; |
|||
|
|||
interface Props extends TabsProps {} |
|||
|
|||
defineOptions({ |
|||
name: 'ChromeTabs', |
|||
}); |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
maxWidth: 150, |
|||
menus: () => [], |
|||
minWidth: 40, |
|||
tabs: () => [], |
|||
}); |
|||
|
|||
const emit = defineEmits<{ close: [string]; unpinTab: [TabItem] }>(); |
|||
|
|||
const gap = 7; |
|||
|
|||
const active = defineModel<string>('active'); |
|||
const { b, e, is } = useNamespace('chrome-tabs'); |
|||
|
|||
const contentRef = ref(); |
|||
const tabWidth = ref<number>(0); |
|||
|
|||
const layout = () => { |
|||
const { maxWidth, minWidth, tabs } = props; |
|||
if (!contentRef.value) { |
|||
return Math.max(maxWidth, minWidth); |
|||
} |
|||
const contentWidth = contentRef.value.clientWidth - gap * 3; |
|||
let width = contentWidth / tabs.length; |
|||
width += gap * 2; |
|||
if (width > maxWidth) { |
|||
width = maxWidth; |
|||
} |
|||
if (width < minWidth) { |
|||
width = minWidth; |
|||
} |
|||
tabWidth.value = width; |
|||
}; |
|||
|
|||
const tabsView = computed(() => { |
|||
return props.tabs.map((tab) => { |
|||
return { |
|||
...tab, |
|||
affixTab: !!tab.meta?.affixTab, |
|||
icon: tab.meta.icon as string, |
|||
key: tab.fullPath || tab.path, |
|||
title: (tab.meta?.title || tab.name) as string, |
|||
}; |
|||
}); |
|||
}); |
|||
|
|||
watch( |
|||
() => props.tabs, |
|||
() => { |
|||
nextTick(() => { |
|||
layout(); |
|||
}); |
|||
}, |
|||
); |
|||
|
|||
onMounted(() => { |
|||
layout(); |
|||
}); |
|||
|
|||
function handleClose(key: string) { |
|||
emit('close', key); |
|||
} |
|||
function handleUnpinTab(tab: TabItem) { |
|||
emit('unpinTab', tab); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="b()" class="relative size-full pt-1"> |
|||
<div ref="contentRef" class="relative h-8 overflow-hidden"> |
|||
<TransitionGroup name="slide-down"> |
|||
<Tab |
|||
v-for="(tab, i) in tabsView" |
|||
:key="tab.key" |
|||
:affix-tab="tab.affixTab" |
|||
:class="[e('tab'), is('active', tab.key === active)]" |
|||
:icon="tab.icon" |
|||
:menus="menus" |
|||
:only-one="tabsView.length <= 1" |
|||
:show-icon="showIcon" |
|||
:style="{ |
|||
width: `${tabWidth}px`, |
|||
left: `${(tabWidth - gap * 2) * i}px`, |
|||
}" |
|||
:tab="tab" |
|||
:title="tab.title" |
|||
@click="active = tab.key" |
|||
@close="() => handleClose(tab.key)" |
|||
@unpin-tab="() => handleUnpinTab(tab)" |
|||
/> |
|||
</TransitionGroup> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<style lang="scss"> |
|||
@import './chrome-tabs.scss'; |
|||
</style> |
|||
@ -1 +1 @@ |
|||
export { default as ChromeTabs } from './chrome-tabs/tabs.vue'; |
|||
export { default as TabsChrome } from './tabs-chrome/tabs.vue'; |
|||
|
|||
@ -0,0 +1,264 @@ |
|||
<script setup lang="ts"> |
|||
import type { TabItem } from '@vben-core/typings'; |
|||
|
|||
import type { TabsProps } from '../../types'; |
|||
|
|||
import { computed, nextTick, onMounted, ref, watch } from 'vue'; |
|||
|
|||
import { MdiPin } from '@vben-core/iconify'; |
|||
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui'; |
|||
|
|||
interface Props extends TabsProps {} |
|||
|
|||
defineOptions({ |
|||
name: 'TabsChrome', |
|||
// eslint-disable-next-line perfectionist/sort-objects |
|||
inheritAttrs: false, |
|||
}); |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
contentClass: 'vben-tabs-content', |
|||
contextMenus: () => [], |
|||
gap: 7, |
|||
maxWidth: 150, |
|||
minWidth: 40, |
|||
tabs: () => [], |
|||
}); |
|||
|
|||
const emit = defineEmits<{ close: [string]; unpin: [TabItem] }>(); |
|||
const active = defineModel<string>('active'); |
|||
|
|||
const contentRef = ref(); |
|||
const tabRef = ref(); |
|||
const tabWidth = ref<number>(0); |
|||
|
|||
const style = computed(() => { |
|||
const { gap } = props; |
|||
return { |
|||
'--gap': `${gap}px`, |
|||
}; |
|||
}); |
|||
|
|||
const layout = () => { |
|||
const { gap, maxWidth, minWidth, tabs } = props; |
|||
if (!contentRef.value) { |
|||
return Math.max(maxWidth, minWidth); |
|||
} |
|||
const contentWidth = contentRef.value.clientWidth - gap * 3; |
|||
let width = contentWidth / tabs.length; |
|||
width += gap * 2; |
|||
if (width > maxWidth) { |
|||
width = maxWidth; |
|||
} |
|||
if (width < minWidth) { |
|||
width = minWidth; |
|||
} |
|||
tabWidth.value = width; |
|||
}; |
|||
|
|||
const tabsView = computed(() => { |
|||
return props.tabs.map((tab) => { |
|||
return { |
|||
...tab, |
|||
affixTab: !!tab.meta?.affixTab, |
|||
closable: tab.meta?.tabClosable ?? true, |
|||
icon: tab.meta.icon as string, |
|||
key: tab.fullPath || tab.path, |
|||
title: (tab.meta?.title || tab.name) as string, |
|||
}; |
|||
}); |
|||
}); |
|||
|
|||
watch( |
|||
() => props.tabs, |
|||
() => { |
|||
nextTick(() => { |
|||
layout(); |
|||
}); |
|||
}, |
|||
); |
|||
|
|||
onMounted(() => { |
|||
layout(); |
|||
}); |
|||
|
|||
function handleClose(key: string) { |
|||
emit('close', key); |
|||
} |
|||
function handleUnpinTab(tab: TabItem) { |
|||
emit('unpin', tab); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div :style="style" class="tabs-chrome bg-accent size-full pt-1"> |
|||
<!-- footer -> 4px --> |
|||
<div |
|||
ref="contentRef" |
|||
:class="contentClass" |
|||
class="relative h-full overflow-hidden" |
|||
> |
|||
<TransitionGroup name="slide-down"> |
|||
<div |
|||
v-for="(tab, i) in tabsView" |
|||
:key="tab.key" |
|||
ref="tabRef" |
|||
:class="[ |
|||
{ 'is-active': tab.key === active, dragable: !tab.affixTab }, |
|||
]" |
|||
: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" |
|||
@click="active = tab.key" |
|||
> |
|||
<VbenContextMenu |
|||
:handler-data="tab" |
|||
:menus="contextMenus" |
|||
:modal="false" |
|||
item-class="pr-6" |
|||
> |
|||
<div class="size-full"> |
|||
<!-- divider --> |
|||
<div |
|||
v-if="i !== 0" |
|||
class="tabs-chrome__divider bg-accent absolute left-[var(--gap)] top-1/2 z-0 h-5 w-[1px] translate-y-[-50%]" |
|||
></div> |
|||
<!-- background --> |
|||
<div |
|||
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 h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150" |
|||
></div> |
|||
<svg |
|||
class="tabs-chrome__background-before absolute bottom-[-1px] left-[-1px] fill-transparent transition-all duration-150" |
|||
height="7" |
|||
width="7" |
|||
> |
|||
<path d="M 0 7 A 7 7 0 0 0 7 0 L 7 7 Z" /> |
|||
</svg> |
|||
<svg |
|||
class="tabs-chrome__background-after absolute bottom-[-1px] right-[-1px] fill-transparent transition-all duration-150" |
|||
height="7" |
|||
width="7" |
|||
> |
|||
<path d="M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z" /> |
|||
</svg> |
|||
</div> |
|||
|
|||
<!-- extra --> |
|||
<div |
|||
class="tabs-chrome__extra absolute right-[calc(var(--gap)*2)] top-1/2 z-[3] size-4 translate-y-[-50%] opacity-0 transition-opacity group-hover:opacity-100" |
|||
> |
|||
<!-- close-icon --> |
|||
<svg |
|||
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable" |
|||
class="hover:bg-accent hover:stroke-accent-foreground size-full cursor-pointer rounded-full transition-all" |
|||
height="12" |
|||
stroke="#595959" |
|||
width="12" |
|||
@click.stop="handleClose(tab.key)" |
|||
> |
|||
<path d="M 4 4 L 12 12 M 12 4 L 4 12" /> |
|||
</svg> |
|||
<MdiPin |
|||
v-show="tab.affixTab && tabsView.length > 1 && tab.closable" |
|||
class="hover:bg-accent hover:stroke-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all" |
|||
@click.stop="handleUnpinTab(tab)" |
|||
/> |
|||
</div> |
|||
|
|||
<!-- tab-item-main --> |
|||
<div |
|||
class="tabs-chrome__item-main 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] duration-150 group-hover:pr-3" |
|||
> |
|||
<VbenIcon |
|||
v-if="showIcon" |
|||
:icon="tab.icon" |
|||
class="ml-[var(--gap)] flex size-4 items-center overflow-hidden" |
|||
fallback |
|||
/> |
|||
|
|||
<span |
|||
class="tabs-chrome__label ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap" |
|||
> |
|||
{{ tab.title }} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</VbenContextMenu> |
|||
</div> |
|||
</TransitionGroup> |
|||
</div> |
|||
<!-- footer --> |
|||
<div class="bg-background h-1"></div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.tabs-chrome { |
|||
.dragging { |
|||
.tabs-chrome__item-main { |
|||
@apply pr-0; |
|||
} |
|||
|
|||
.tabs-chrome__extra { |
|||
@apply hidden; |
|||
} |
|||
} |
|||
|
|||
&__item { |
|||
&:hover { |
|||
& + .tabs-chrome__item { |
|||
.tabs-chrome__divider { |
|||
@apply opacity-0; |
|||
} |
|||
} |
|||
|
|||
.tabs-chrome__divider { |
|||
@apply opacity-0; |
|||
} |
|||
|
|||
.tabs-chrome__background { |
|||
&-content { |
|||
@apply bg-accent; |
|||
} |
|||
|
|||
&-before, |
|||
&-after { |
|||
@apply fill-accent; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&.is-active { |
|||
@apply z-[2]; |
|||
|
|||
.tabs-chrome__background { |
|||
@apply opacity-100; |
|||
|
|||
&-content { |
|||
@apply bg-background; |
|||
} |
|||
|
|||
&-before, |
|||
&-after { |
|||
@apply fill-background; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
&__label { |
|||
mask-image: linear-gradient( |
|||
90deg, |
|||
#000 0%, |
|||
#000 calc(100% - 16px), |
|||
transparent |
|||
); |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,3 +1,3 @@ |
|||
export * from './components/widgets'; |
|||
export { default as TabsView } from './tabs-view.vue'; |
|||
export * from './widgets'; |
|||
export type { IContextMenuItem } from '@vben-core/shadcn-ui'; |
|||
|
|||
@ -1,3 +0,0 @@ |
|||
import { defineConfig } from '@vben/vite-config'; |
|||
|
|||
export default defineConfig(); |
|||
@ -1,3 +1,2 @@ |
|||
export { default as LayoutTabbar } from './tabbar.vue'; |
|||
export { default as LayoutTabbarTools } from './tabbar-tools.vue'; |
|||
export * from './use-tabs'; |
|||
|
|||
@ -1,28 +0,0 @@ |
|||
<script lang="ts" setup> |
|||
import { computed } from 'vue'; |
|||
import { useRoute } from 'vue-router'; |
|||
|
|||
import { preferences } from '@vben-core/preferences'; |
|||
import { TabsToolMore, TabsToolScreen } from '@vben-core/tabs-ui'; |
|||
|
|||
import { updateContentScreen, useTabs } from './use-tabs'; |
|||
|
|||
const route = useRoute(); |
|||
|
|||
const { createContextMenus } = useTabs(); |
|||
|
|||
const menus = computed(() => { |
|||
return createContextMenus(route); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="flex-center h-full"> |
|||
<TabsToolMore :menus="menus" /> |
|||
<TabsToolScreen |
|||
:screen="preferences.sidebar.hidden" |
|||
@change="updateContentScreen" |
|||
@update:screen="updateContentScreen" |
|||
/> |
|||
</div> |
|||
</template> |
|||
Loading…
Reference in new issue