vue3-router-tab 1.3.5 β 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +529 -47
- package/dist/vue3-router-tab.css +1 -1
- package/dist/vue3-router-tab.js +621 -572
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +56 -4
- package/lib/core/createRouterTabs.ts +37 -7
- package/lib/core/types.ts +5 -0
- package/lib/scss/index.scss +22 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,36 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Vue3 Router Tab
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/js/vue3-router-tab)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
A powerful, feature-rich Vue 3 tab-bar plugin that keeps multiple routes alive with smooth transitions, context menus, drag-and-drop reordering, and optional cookie-based persistence. Built for modern Vue 3 applications with full TypeScript support.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## β¨ Key Features
|
|
10
|
+
|
|
11
|
+
- π― **Multi-tab Navigation** - Keep multiple routes alive simultaneously with intelligent caching
|
|
8
12
|
- π **7 Built-in Transitions** - Smooth page transitions (swap, slide, fade, scale, flip, rotate, bounce)
|
|
9
13
|
- π¨ **Reactive Tab Titles** - Automatically update tab titles, icons, and closability from component state
|
|
10
14
|
- π±οΈ **Context Menu** - Right-click tabs for refresh, close, and navigation options
|
|
11
|
-
- π **Drag & Drop** - Reorder tabs with drag-and-drop
|
|
12
|
-
- πΎ **Cookie Persistence** - Restore tabs on page refresh
|
|
15
|
+
- π **Drag & Drop** - Reorder tabs with drag-and-drop (sortable)
|
|
16
|
+
- πΎ **Cookie Persistence** - Restore tabs on page refresh with customizable options
|
|
13
17
|
- π **Theme Support** - Light, dark, and system themes with customizable colors
|
|
14
|
-
- β‘ **KeepAlive Support** - Preserve component state when switching tabs
|
|
15
|
-
-
|
|
16
|
-
-
|
|
18
|
+
- β‘ **KeepAlive Support** - Preserve component state when switching tabs with smart cache management
|
|
19
|
+
- βΏ **Accessibility** - Full WCAG compliance with ARIA labels, keyboard navigation, and screen reader support
|
|
20
|
+
- π **Performance Optimized** - Intelligent caching, memoization, and memory management
|
|
21
|
+
- ποΈ **Highly Configurable** - Extensive props, events, and customization options
|
|
22
|
+
- π± **TypeScript Support** - Full TypeScript definitions with excellent developer experience
|
|
23
|
+
- π§ **Error Recovery** - Automatic error handling with graceful degradation and recovery mechanisms
|
|
17
24
|
|
|
18
|
-
## Installation
|
|
25
|
+
## π¦ Installation
|
|
19
26
|
|
|
20
27
|
```bash
|
|
21
28
|
npm install vue3-router-tab
|
|
22
29
|
# or
|
|
23
30
|
pnpm add vue3-router-tab
|
|
31
|
+
# or
|
|
32
|
+
yarn add vue3-router-tab
|
|
24
33
|
```
|
|
25
34
|
|
|
26
|
-
##
|
|
35
|
+
## π Quick Start
|
|
36
|
+
|
|
37
|
+
### 1. Register the Plugin
|
|
27
38
|
|
|
28
39
|
```ts
|
|
29
40
|
// main.ts
|
|
30
41
|
import { createApp } from 'vue'
|
|
31
42
|
import App from './App.vue'
|
|
32
43
|
import router from './router'
|
|
33
|
-
|
|
34
44
|
import RouterTab from 'vue3-router-tab'
|
|
35
45
|
|
|
36
46
|
const app = createApp(App)
|
|
@@ -39,80 +49,552 @@ app.use(RouterTab)
|
|
|
39
49
|
app.mount('#app')
|
|
40
50
|
```
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## Basic Usage
|
|
52
|
+
### 2. Basic Usage
|
|
45
53
|
|
|
46
54
|
```vue
|
|
47
55
|
<template>
|
|
48
|
-
<router-tab
|
|
56
|
+
<router-tab />
|
|
49
57
|
</template>
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
That's it! You now have a fully functional tabbed router interface.
|
|
53
61
|
|
|
54
|
-
### Enhanced Usage
|
|
62
|
+
### 3. Enhanced Usage with Persistence
|
|
55
63
|
|
|
56
64
|
```vue
|
|
57
65
|
<template>
|
|
58
|
-
<router-tab
|
|
59
|
-
cookie-key="app-tabs"
|
|
66
|
+
<router-tab
|
|
67
|
+
cookie-key="my-app-tabs"
|
|
60
68
|
:sortable="true"
|
|
61
|
-
|
|
62
|
-
@tab-sorted="onTabSorted"
|
|
69
|
+
:keep-alive="true"
|
|
63
70
|
/>
|
|
64
71
|
</template>
|
|
65
|
-
|
|
66
|
-
<script setup>
|
|
67
|
-
function onTabSort({ tab, index }) {
|
|
68
|
-
console.log('Tab drag started:', tab.title, 'at index', index)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function onTabSorted({ tab, fromIndex, toIndex }) {
|
|
72
|
-
console.log('Tab moved:', tab.title, 'from', fromIndex, 'to', toIndex)
|
|
73
|
-
}
|
|
74
|
-
</script>
|
|
75
72
|
```
|
|
76
73
|
|
|
77
|
-
|
|
74
|
+
## π Usage Guide
|
|
75
|
+
|
|
76
|
+
### Basic Configuration
|
|
78
77
|
|
|
79
78
|
```vue
|
|
80
|
-
<
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
<template>
|
|
80
|
+
<router-tab
|
|
81
|
+
cookie-key="app-tabs"
|
|
82
|
+
:keep-alive="true"
|
|
83
|
+
:max-alive="10"
|
|
84
|
+
:keep-last-tab="true"
|
|
85
|
+
:sortable="true"
|
|
86
|
+
page-transition="router-tab-fade"
|
|
87
|
+
tab-transition="router-tab-zoom"
|
|
88
|
+
/>
|
|
89
|
+
</template>
|
|
87
90
|
```
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
### Route Configuration
|
|
93
|
+
|
|
94
|
+
Configure your routes with tab metadata:
|
|
90
95
|
|
|
91
96
|
```ts
|
|
97
|
+
// router/index.ts
|
|
92
98
|
const routes = [
|
|
93
99
|
{
|
|
94
100
|
path: '/',
|
|
95
101
|
component: Home,
|
|
96
102
|
meta: {
|
|
97
103
|
title: 'Home',
|
|
98
|
-
icon: '
|
|
99
|
-
|
|
104
|
+
icon: 'mdi-home',
|
|
105
|
+
closable: true,
|
|
106
|
+
keepAlive: true,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
path: '/users',
|
|
111
|
+
component: Users,
|
|
112
|
+
meta: {
|
|
113
|
+
title: 'Users',
|
|
114
|
+
icon: 'mdi-account-group',
|
|
100
115
|
closable: true,
|
|
101
116
|
keepAlive: true,
|
|
102
117
|
},
|
|
103
118
|
},
|
|
104
119
|
{
|
|
105
|
-
path: '/
|
|
106
|
-
component:
|
|
120
|
+
path: '/settings',
|
|
121
|
+
component: Settings,
|
|
107
122
|
meta: {
|
|
108
|
-
title: '
|
|
109
|
-
icon: '
|
|
123
|
+
title: 'Settings',
|
|
124
|
+
icon: 'mdi-cog',
|
|
125
|
+
closable: false, // Can't be closed
|
|
110
126
|
keepAlive: false,
|
|
111
127
|
},
|
|
112
128
|
},
|
|
113
129
|
]
|
|
114
130
|
```
|
|
115
131
|
|
|
132
|
+
### Reactive Tab Properties
|
|
133
|
+
|
|
134
|
+
Make your tabs dynamic by exposing reactive properties in your components:
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<template>
|
|
138
|
+
<div>
|
|
139
|
+
<h1>{{ pageTitle }}</h1>
|
|
140
|
+
<button @click="updateTitle">Update Title</button>
|
|
141
|
+
<div v-if="loading">Loading...</div>
|
|
142
|
+
</div>
|
|
143
|
+
</template>
|
|
144
|
+
|
|
145
|
+
<script setup lang="ts">
|
|
146
|
+
import { ref, computed } from 'vue'
|
|
147
|
+
|
|
148
|
+
// These reactive properties automatically update the tab
|
|
149
|
+
const pageTitle = ref('Dashboard')
|
|
150
|
+
const loading = ref(false)
|
|
151
|
+
const notificationCount = ref(0)
|
|
152
|
+
|
|
153
|
+
// Tab title updates automatically
|
|
154
|
+
const routeTabTitle = computed(() => {
|
|
155
|
+
if (loading.value) return 'Loading...'
|
|
156
|
+
if (notificationCount.value > 0) return `Dashboard (${notificationCount.value})`
|
|
157
|
+
return pageTitle.value
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Tab icon changes based on state
|
|
161
|
+
const routeTabIcon = computed(() =>
|
|
162
|
+
loading.value ? 'mdi-loading mdi-spin' : 'mdi-view-dashboard'
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// Prevent closing while loading
|
|
166
|
+
const routeTabClosable = computed(() => !loading.value)
|
|
167
|
+
|
|
168
|
+
function updateTitle() {
|
|
169
|
+
pageTitle.value = 'Updated Dashboard'
|
|
170
|
+
}
|
|
171
|
+
</script>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## π¨ Transitions
|
|
175
|
+
|
|
176
|
+
Choose from 7 built-in transition effects:
|
|
177
|
+
|
|
178
|
+
```vue
|
|
179
|
+
<!-- Default transition -->
|
|
180
|
+
<router-tab />
|
|
181
|
+
|
|
182
|
+
<!-- Custom transition -->
|
|
183
|
+
<router-tab page-transition="router-tab-scale" />
|
|
184
|
+
|
|
185
|
+
<!-- Advanced configuration -->
|
|
186
|
+
<router-tab :page-transition="{ name: 'router-tab-flip', mode: 'out-in' }" />
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Available Transitions
|
|
190
|
+
|
|
191
|
+
| Transition | Description | Best For |
|
|
192
|
+
|------------|-------------|----------|
|
|
193
|
+
| `router-tab-swap` | Smooth up/down slide with fade | General purpose (default) |
|
|
194
|
+
| `router-tab-slide` | Horizontal sliding | Dashboard navigation |
|
|
195
|
+
| `router-tab-fade` | Simple opacity fade | Minimal, subtle |
|
|
196
|
+
| `router-tab-scale` | Zoom in/out effect | Dramatic transitions |
|
|
197
|
+
| `router-tab-flip` | 3D flip animation | Modern, creative |
|
|
198
|
+
| `router-tab-rotate` | Rotation with scale | Playful, dynamic |
|
|
199
|
+
| `router-tab-bounce` | Elastic bounce | Fun, energetic |
|
|
200
|
+
|
|
201
|
+
## π Theming
|
|
202
|
+
|
|
203
|
+
### Built-in Themes
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { setRouterTabsTheme } from 'vue3-router-tab'
|
|
207
|
+
|
|
208
|
+
// Switch themes at runtime
|
|
209
|
+
setRouterTabsTheme('dark')
|
|
210
|
+
setRouterTabsTheme('light')
|
|
211
|
+
setRouterTabsTheme('system') // Follows OS preference
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Custom Colors
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { setRouterTabsPrimary } from 'vue3-router-tab'
|
|
218
|
+
|
|
219
|
+
setRouterTabsPrimary({
|
|
220
|
+
primary: '#3b82f6',
|
|
221
|
+
background: '#ffffff',
|
|
222
|
+
text: '#1f2937',
|
|
223
|
+
activeBackground: '#3b82f6',
|
|
224
|
+
activeText: '#ffffff',
|
|
225
|
+
border: '#e5e7eb'
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### CSS Customization
|
|
230
|
+
|
|
231
|
+
```css
|
|
232
|
+
:root {
|
|
233
|
+
/* Layout */
|
|
234
|
+
--router-tab-header-height: 48px;
|
|
235
|
+
--router-tab-padding: 16px;
|
|
236
|
+
|
|
237
|
+
/* Colors */
|
|
238
|
+
--router-tab-primary: #3b82f6;
|
|
239
|
+
--router-tab-background: #ffffff;
|
|
240
|
+
--router-tab-active-background: #3b82f6;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## βΏ Accessibility
|
|
245
|
+
|
|
246
|
+
Vue3 Router Tab is fully accessible with:
|
|
247
|
+
|
|
248
|
+
- **ARIA Labels**: Proper labeling for screen readers
|
|
249
|
+
- **Keyboard Navigation**: Arrow keys, Enter, Delete, Home, End
|
|
250
|
+
- **Focus Management**: Logical tab order and focus indicators
|
|
251
|
+
- **Semantic HTML**: Proper roles and structure
|
|
252
|
+
|
|
253
|
+
```vue
|
|
254
|
+
<!-- Accessible by default -->
|
|
255
|
+
<router-tab />
|
|
256
|
+
|
|
257
|
+
<!-- Custom ARIA labels -->
|
|
258
|
+
<router-tab aria-label="Main navigation tabs" />
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Keyboard Shortcuts
|
|
262
|
+
|
|
263
|
+
- **Arrow Keys**: Navigate between tabs
|
|
264
|
+
- **Enter/Space**: Activate selected tab
|
|
265
|
+
- **Delete/Backspace**: Close current tab (if closable)
|
|
266
|
+
- **Home/End**: Jump to first/last tab
|
|
267
|
+
|
|
268
|
+
## π§ API Reference
|
|
269
|
+
|
|
270
|
+
### Props
|
|
271
|
+
|
|
272
|
+
| Prop | Type | Default | Description |
|
|
273
|
+
|------|------|---------|-------------|
|
|
274
|
+
| `tabs` | `TabInput[]` | `[]` | Initial tabs to display |
|
|
275
|
+
| `keepAlive` | `boolean` | `true` | Enable KeepAlive for tab components |
|
|
276
|
+
| `maxAlive` | `number` | `0` | Maximum cached components (0 = unlimited) |
|
|
277
|
+
| `keepLastTab` | `boolean` | `true` | Prevent closing the last tab |
|
|
278
|
+
| `append` | `'last' \| 'next'` | `'last'` | Position for new tabs |
|
|
279
|
+
| `defaultPage` | `RouteLocationRaw` | `'/'` | Default route |
|
|
280
|
+
| `tabTransition` | `TransitionLike` | `'router-tab-zoom'` | Tab list transitions |
|
|
281
|
+
| `pageTransition` | `TransitionLike` | `{ name: 'router-tab-swap', mode: 'out-in' }` | Page transitions |
|
|
282
|
+
| `contextmenu` | `boolean \| RouterTabsMenuConfig[]` | `true` | Context menu configuration |
|
|
283
|
+
| `cookieKey` | `string` | `'router-tabs:snapshot'` | Persistence cookie key |
|
|
284
|
+
| `persistence` | `RouterTabsPersistenceOptions \| null` | `null` | Advanced persistence options |
|
|
285
|
+
| `sortable` | `boolean` | `true` | Enable drag-and-drop sorting |
|
|
286
|
+
|
|
287
|
+
### Events
|
|
288
|
+
|
|
289
|
+
| Event | Payload | Description |
|
|
290
|
+
|-------|---------|-------------|
|
|
291
|
+
| `tab-sort` | `{ tab: TabRecord, index: number }` | Tab drag started |
|
|
292
|
+
| `tab-sorted` | `{ tab: TabRecord, fromIndex: number, toIndex: number }` | Tab reordered |
|
|
293
|
+
|
|
294
|
+
### Slots
|
|
295
|
+
|
|
296
|
+
| Slot | Props | Description |
|
|
297
|
+
|------|-------|-------------|
|
|
298
|
+
| `start` | - | Content before tab list |
|
|
299
|
+
| `end` | - | Content after tab list |
|
|
300
|
+
| `default` | `{ Component, route }` | Custom page rendering |
|
|
301
|
+
|
|
302
|
+
## ποΈ Programmatic API
|
|
303
|
+
|
|
304
|
+
### Using the Composable
|
|
305
|
+
|
|
306
|
+
```vue
|
|
307
|
+
<script setup lang="ts">
|
|
308
|
+
import { useRouterTabs } from 'vue3-router-tab'
|
|
309
|
+
|
|
310
|
+
const tabs = useRouterTabs()
|
|
311
|
+
|
|
312
|
+
// Open a new tab
|
|
313
|
+
await tabs.openTab('/users')
|
|
314
|
+
|
|
315
|
+
// Close current tab
|
|
316
|
+
await tabs.closeTab()
|
|
317
|
+
|
|
318
|
+
// Refresh all tabs
|
|
319
|
+
await tabs.refreshAll()
|
|
320
|
+
|
|
321
|
+
// Get current tab info
|
|
322
|
+
console.log(tabs.activeId.value) // Current active tab ID
|
|
323
|
+
console.log(tabs.tabs) // All tabs array
|
|
324
|
+
</script>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Controller Methods
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
interface RouterTabsContext {
|
|
331
|
+
// Navigation
|
|
332
|
+
openTab(to: RouteLocationRaw, replace?: boolean, refresh?: boolean | 'sameTab'): Promise<void>
|
|
333
|
+
closeTab(id?: string, options?: CloseTabOptions): Promise<void>
|
|
334
|
+
|
|
335
|
+
// Management
|
|
336
|
+
refreshTab(id?: string, force?: boolean): Promise<void>
|
|
337
|
+
refreshAll(force?: boolean): Promise<void>
|
|
338
|
+
removeTab(id: string, opts?: RemoveTabOptions): Promise<void>
|
|
339
|
+
|
|
340
|
+
// Cache Control
|
|
341
|
+
setTabAlive(id: string, alive: boolean): void
|
|
342
|
+
evictCache(id: string): void
|
|
343
|
+
clearCache(): void
|
|
344
|
+
getCacheKeys(): string[]
|
|
345
|
+
|
|
346
|
+
// State
|
|
347
|
+
reset(route?: RouteLocationRaw): Promise<void>
|
|
348
|
+
reload(): Promise<void>
|
|
349
|
+
|
|
350
|
+
// Utilities
|
|
351
|
+
getRouteKey(route: RouteLocationNormalizedLoaded | RouteLocationRaw): string
|
|
352
|
+
matchRoute(route: RouteLocationNormalizedLoaded | RouteLocationRaw): RouteMatchResult
|
|
353
|
+
|
|
354
|
+
// Persistence
|
|
355
|
+
snapshot(): RouterTabsSnapshot
|
|
356
|
+
hydrate(snapshot: RouterTabsSnapshot): Promise<void>
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## π Advanced Usage
|
|
361
|
+
|
|
362
|
+
### Custom Context Menu
|
|
363
|
+
|
|
364
|
+
```vue
|
|
365
|
+
<router-tab
|
|
366
|
+
:contextmenu="[
|
|
367
|
+
'refresh',
|
|
368
|
+
'close',
|
|
369
|
+
{ id: 'duplicate', label: 'Duplicate Tab', handler: ({ target }) => openTab(target.to) },
|
|
370
|
+
{ id: 'closeOthers', label: 'Close All Others' },
|
|
371
|
+
{
|
|
372
|
+
id: 'openExternal',
|
|
373
|
+
label: 'Open in New Window',
|
|
374
|
+
handler: ({ target }) => window.open(target.to, '_blank')
|
|
375
|
+
}
|
|
376
|
+
]"
|
|
377
|
+
/>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Custom Rendering
|
|
381
|
+
|
|
382
|
+
```vue
|
|
383
|
+
<router-tab>
|
|
384
|
+
<template #default="{ Component, route }">
|
|
385
|
+
<Suspense>
|
|
386
|
+
<ErrorBoundary>
|
|
387
|
+
<component :is="Component" :key="route.fullPath" />
|
|
388
|
+
</ErrorBoundary>
|
|
389
|
+
</Suspense>
|
|
390
|
+
</template>
|
|
391
|
+
</router-tab>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Advanced Persistence
|
|
395
|
+
|
|
396
|
+
```vue
|
|
397
|
+
<router-tab
|
|
398
|
+
:persistence="{
|
|
399
|
+
cookieKey: 'my-app-tabs',
|
|
400
|
+
expiresInDays: 30,
|
|
401
|
+
path: '/',
|
|
402
|
+
secure: true,
|
|
403
|
+
sameSite: 'strict',
|
|
404
|
+
serialize: (snapshot) => encrypt(JSON.stringify(snapshot)),
|
|
405
|
+
deserialize: (data) => JSON.parse(decrypt(data))
|
|
406
|
+
}"
|
|
407
|
+
/>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## π§© Composables
|
|
411
|
+
|
|
412
|
+
### useReactiveTab
|
|
413
|
+
|
|
414
|
+
```vue
|
|
415
|
+
<script setup lang="ts">
|
|
416
|
+
import { useReactiveTab } from 'vue3-router-tab'
|
|
417
|
+
|
|
418
|
+
const { routeTabTitle, routeTabIcon, routeTabClosable } = useReactiveTab({
|
|
419
|
+
title: 'Dashboard',
|
|
420
|
+
icon: 'mdi-view-dashboard',
|
|
421
|
+
closable: true
|
|
422
|
+
})
|
|
423
|
+
</script>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### useLoadingTab
|
|
427
|
+
|
|
428
|
+
```vue
|
|
429
|
+
<script setup lang="ts">
|
|
430
|
+
import { useLoadingTab } from 'vue3-router-tab'
|
|
431
|
+
|
|
432
|
+
const isLoading = ref(false)
|
|
433
|
+
const { routeTabTitle, routeTabIcon, routeTabClosable } = useLoadingTab(isLoading, 'Dashboard')
|
|
434
|
+
</script>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### useNotificationTab
|
|
438
|
+
|
|
439
|
+
```vue
|
|
440
|
+
<script setup lang="ts">
|
|
441
|
+
import { useNotificationTab } from 'vue3-router-tab'
|
|
442
|
+
|
|
443
|
+
const notificationCount = ref(0)
|
|
444
|
+
const { routeTabTitle, routeTabIcon } = useNotificationTab(notificationCount, 'Messages')
|
|
445
|
+
</script>
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## π― Examples
|
|
449
|
+
|
|
450
|
+
### Complete App Example
|
|
451
|
+
|
|
452
|
+
```vue
|
|
453
|
+
<template>
|
|
454
|
+
<div id="app">
|
|
455
|
+
<nav>
|
|
456
|
+
<router-link to="/">Home</router-link>
|
|
457
|
+
<router-link to="/users">Users</router-link>
|
|
458
|
+
<router-link to="/settings">Settings</router-link>
|
|
459
|
+
</nav>
|
|
460
|
+
|
|
461
|
+
<router-tab
|
|
462
|
+
cookie-key="my-app"
|
|
463
|
+
:sortable="true"
|
|
464
|
+
page-transition="router-tab-fade"
|
|
465
|
+
@tab-sorted="onTabSorted"
|
|
466
|
+
/>
|
|
467
|
+
</div>
|
|
468
|
+
</template>
|
|
469
|
+
|
|
470
|
+
<script setup lang="ts">
|
|
471
|
+
import { useRouterTabs } from 'vue3-router-tab'
|
|
472
|
+
|
|
473
|
+
const tabs = useRouterTabs()
|
|
474
|
+
|
|
475
|
+
function onTabSorted({ tab, fromIndex, toIndex }) {
|
|
476
|
+
console.log(`Tab "${tab.title}" moved from ${fromIndex} to ${toIndex}`)
|
|
477
|
+
// Save order to backend
|
|
478
|
+
saveTabOrder(tab.id, toIndex)
|
|
479
|
+
}
|
|
480
|
+
</script>
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Real-time Updates
|
|
484
|
+
|
|
485
|
+
```vue
|
|
486
|
+
<script setup lang="ts">
|
|
487
|
+
import { ref, computed, onMounted } from 'vue'
|
|
488
|
+
import { useRouterTabs } from 'vue3-router-tab'
|
|
489
|
+
|
|
490
|
+
const tabs = useRouterTabs()
|
|
491
|
+
const messageCount = ref(0)
|
|
492
|
+
const isOnline = ref(true)
|
|
493
|
+
|
|
494
|
+
// Reactive tab title
|
|
495
|
+
const routeTabTitle = computed(() =>
|
|
496
|
+
messageCount.value > 0 ? `Messages (${messageCount.value})` : 'Messages'
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
// Reactive tab icon
|
|
500
|
+
const routeTabIcon = computed(() =>
|
|
501
|
+
isOnline.value ? 'mdi-message' : 'mdi-message-offline'
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
// Simulate real-time updates
|
|
505
|
+
onMounted(() => {
|
|
506
|
+
// Connect to WebSocket or polling service
|
|
507
|
+
connectToRealtimeUpdates((update) => {
|
|
508
|
+
if (update.type === 'message') {
|
|
509
|
+
messageCount.value++
|
|
510
|
+
} else if (update.type === 'status') {
|
|
511
|
+
isOnline.value = update.online
|
|
512
|
+
}
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
</script>
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
## π Troubleshooting
|
|
519
|
+
|
|
520
|
+
### Common Issues
|
|
521
|
+
|
|
522
|
+
**Tabs not updating titles**
|
|
523
|
+
- Ensure you're using `ref()` or `computed()` for reactive properties
|
|
524
|
+
- Check that properties are properly exposed in `<script setup>`
|
|
525
|
+
|
|
526
|
+
**Transitions not working**
|
|
527
|
+
- Don't use custom `#default` slot without transitions
|
|
528
|
+
- Ensure transition names match available transitions
|
|
529
|
+
|
|
530
|
+
**Persistence not working**
|
|
531
|
+
- Check that cookies are enabled
|
|
532
|
+
- Verify `cookie-key` prop is set
|
|
533
|
+
- Check browser console for cookie errors
|
|
534
|
+
|
|
535
|
+
**KeepAlive not preserving state**
|
|
536
|
+
- Ensure routes have `keepAlive: true` in meta
|
|
537
|
+
- Check that components have unique keys
|
|
538
|
+
|
|
539
|
+
**TypeScript errors**
|
|
540
|
+
```ts
|
|
541
|
+
import type { TabRecord, RouterTabsOptions } from 'vue3-router-tab'
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Performance Tips
|
|
545
|
+
|
|
546
|
+
- Use `maxAlive` to limit cached components
|
|
547
|
+
- Avoid deep watchers in tab components
|
|
548
|
+
- Use `evictCache()` to manually clear cache when needed
|
|
549
|
+
- Consider using `keepAlive: false` for memory-intensive components
|
|
550
|
+
|
|
551
|
+
## π Migration Guide
|
|
552
|
+
|
|
553
|
+
### From v1.x to v2.x
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
// Before
|
|
557
|
+
import RouterTab from 'vue3-router-tab'
|
|
558
|
+
|
|
559
|
+
// After (same)
|
|
560
|
+
import RouterTab from 'vue3-router-tab'
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
The API is backward compatible. New features are additive.
|
|
564
|
+
|
|
565
|
+
## π± Browser Support
|
|
566
|
+
|
|
567
|
+
- **Chrome**: 90+
|
|
568
|
+
- **Firefox**: 88+
|
|
569
|
+
- **Safari**: 14+
|
|
570
|
+
- **Edge**: 90+
|
|
571
|
+
- **Mobile**: iOS Safari 14+, Chrome Mobile 90+
|
|
572
|
+
|
|
573
|
+
## π€ Contributing
|
|
574
|
+
|
|
575
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
576
|
+
|
|
577
|
+
1. Fork the repository
|
|
578
|
+
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
|
579
|
+
3. Make your changes
|
|
580
|
+
4. Add tests if applicable
|
|
581
|
+
5. Run the build: `npm run build`
|
|
582
|
+
6. Commit your changes: `git commit -m 'Add amazing feature'`
|
|
583
|
+
7. Push to the branch: `git push origin feature/amazing-feature`
|
|
584
|
+
8. Open a Pull Request
|
|
585
|
+
|
|
586
|
+
## π License
|
|
587
|
+
|
|
588
|
+
MIT License - see the [LICENSE](LICENSE) file for details.
|
|
589
|
+
|
|
590
|
+
## π Acknowledgments
|
|
591
|
+
|
|
592
|
+
Built with β€οΈ by the Vue.js community. Special thanks to all contributors and users.
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
**Made with β€οΈ for the Vue.js ecosystem**
|
|
597
|
+
|
|
116
598
|
`meta.key` accepts the shortcuts `fullPath`, `path`, or `name`, or you can supply your own function.
|
|
117
599
|
|
|
118
600
|
## Configuration
|
package/dist/vue3-router-tab.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--router-tab-header-height: 42px;--router-tab-padding: 12px;--router-tab-font-size: 14px;--router-tab-transition: all .3s ease;--router-tab-primary: #3b82f6;--router-tab-background: #ffffff;--router-tab-text: #0f172a;--router-tab-border: #e2e8f0;--router-tab-header-bg: #ffffff;--router-tab-active-background: #3b82f6;--router-tab-active-text: #ffffff;--router-tab-active-border: #3b82f6;--router-tab-icon-color: #475569;--router-tab-button-background: #f8fafc;--router-tab-button-color: #0f172a;--router-tab-active-button-background: #3b82f6;--router-tab-active-button-color: #ffffff}:root[data-theme=dark]{--router-tab-background: #1e293b;--router-tab-text: #f1f5f9;--router-tab-border: #334155;--router-tab-header-bg: #1e293b;--router-tab-icon-color: #cbd5e1;--router-tab-button-background: #1f2937;--router-tab-button-color: #f8fafc;--router-tab-active-button-background: #38bdf8;--router-tab-active-button-color: #0f172a}.router-tab{display:flex;flex-direction:column;min-height:300px;background-color:var(--router-tab-background);color:var(--router-tab-text)}.router-tab__header{position:relative;z-index:10;display:flex;flex:none;align-items:center;box-sizing:border-box;height:var(--router-tab-header-height);background-color:var(--router-tab-header-bg);border-bottom:1px solid var(--router-tab-border);transition:all .2s ease}.router-tab__scroll{position:relative;flex:1 1 0px;height:100%;overflow:hidden}.router-tab__scroll-container{width:100%;height:100%;overflow:hidden}.router-tab__scroll-container.is-mobile{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.router-tab__scroll-container.is-mobile::-webkit-scrollbar{display:none}.router-tab__nav{position:relative;display:inline-flex;flex-wrap:nowrap;height:100%;margin:0;padding:0;list-style:none}.router-tab__item{position:relative;display:flex;flex:none;align-items:center;gap:.5rem;padding:0 var(--router-tab-padding);color:var(--router-tab-text);font-size:var(--router-tab-font-size);background-color:transparent;border:none;border-right:1px solid var(--router-tab-border);cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--router-tab-transition);will-change:background-color,color}.router-tab__item:first-child{border-left:1px solid var(--router-tab-border)}.router-tab__item:hover{background-color:color-mix(in srgb,var(--router-tab-primary) 4%,transparent);color:var(--router-tab-primary)}.router-tab__item.is-active{background-color:var(--router-tab-active-background);color:var(--router-tab-active-text);font-weight:510}.router-tab__item.is-active:after{content:"";position:absolute;bottom:-1px;left:0;right:0;height:2px;background-color:var(--router-tab-active-border)}.router-tab__item.is-active+.router-tab__item{border-left-color:transparent}.router-tab__item-icon{flex-shrink:0;font-size:14px;color:var(--router-tab-icon-color)}.router-tab__item.is-active .router-tab__item-icon{color:var(--router-tab-active-text)}.router-tab__item:hover .router-tab__item-icon{color:currentColor}.router-tab__item-title{min-width:auto;max-width:180px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.router-tab__item-close{position:relative;display:block;width:0;height:12px;flex-shrink:0;overflow:hidden;border:none;border-radius:50%;background:var(--router-tab-button-background);color:var(--router-tab-button-color);cursor:pointer;transition:var(--router-tab-transition)}.router-tab__item-close:before,.router-tab__item-close:after{position:absolute;top:50%;left:50%;display:block;width:8px;height:1px;margin-left:-4px;background-color:currentColor;transition:background-color .2s ease-in-out;content:""}.router-tab__item-close:before{transform:translateY(-50%) rotate(-45deg)}.router-tab__item-close:after{transform:translateY(-50%) rotate(45deg)}.router-tab__item-close:hover{background:var(--router-tab-active-button-background);color:var(--router-tab-active-button-color)}.router-tab__item:hover .router-tab__item-close,.router-tab__item.is-active .router-tab__item-close{width:12px;margin-left:4px}.router-tab__item.is-dragging{opacity:.6;cursor:grabbing}.router-tab__item.is-drag-over{background-color:color-mix(in srgb,var(--router-tab-primary) 12%,transparent)}.router-tab__container{position:relative;flex:1 1 auto;background-color:var(--router-tab-background);overflow:hidden}.router-tab__container{padding:10px;border:1px solid var(--router-tab-border);transition:var(--router-tab-transition);will-change:background-color,border-color}.router-tab__slot-start,.router-tab__slot-end{display:none;align-items:center;gap:.25rem;padding:0 .5rem;height:100%}.router-tab__slot-start.has-content{display:flex;border-right:1px solid var(--router-tab-border)}.router-tab__slot-end.has-content{display:flex;border-left:1px solid var(--router-tab-border)}.router-tab__contextmenu{position:fixed;z-index:1000;min-width:160px;padding:6px 0;font-size:var(--router-tab-font-size);background:var(--router-tab-background);border:1px solid var(--router-tab-border);border-radius:8px;box-shadow:0 2px 8px #0000001a}.router-tab__contextmenu-item{display:block;width:100%;padding:5px 12px;line-height:32px;text-align:left;text-decoration:none;background:var(--router-tab-button-background);border:none;color:var(--router-tab-button-color);cursor:pointer;font:inherit;border-radius:4px;transition:all .2s ease-in-out;outline:none}.router-tab__contextmenu-item[aria-disabled=true]{color:color-mix(in srgb,var(--router-tab-text) 40%);pointer-events:none;cursor:not-allowed}.router-tab__contextmenu-item:focus,.router-tab__contextmenu-item:focus-visible{outline:none}.router-tab__contextmenu-item:hover:not([aria-disabled=true]),.router-tab__contextmenu-item:focus-visible,.router-tab__contextmenu-item.is-focused{background:var(--router-tab-active-button-background);color:var(--router-tab-active-button-color)}.router-tab-zoom-enter-active,.router-tab-zoom-leave-active{transition:transform all .2s ease,opacity all .2s ease}.router-tab-zoom-enter-from,.router-tab-zoom-leave-to{transform:scale(.9);opacity:0}.router-tab-zoom-enter-to,.router-tab-zoom-leave-from{transform:scale(1);opacity:1}.router-tab-zoom-move{transition:transform all .2s ease}.router-tab-swap-enter-active,.router-tab-swap-leave-active{position:relative;transition:opacity .3s ease,transform .3s ease;will-change:opacity,transform}.router-tab-swap-enter-from{opacity:0;transform:translateY(20px)}.router-tab-swap-enter-to,.router-tab-swap-leave-from{opacity:1;transform:translateY(0)}.router-tab-swap-leave-to{opacity:0;transform:translateY(-15px)}.router-tab-slide-enter-active,.router-tab-slide-leave-active{position:relative;transition:opacity .3s ease,transform .3s ease;will-change:opacity,transform}.router-tab-slide-enter-from{opacity:0;transform:translate(80px)}.router-tab-slide-enter-to,.router-tab-slide-leave-from{opacity:1;transform:translate(0)}.router-tab-slide-leave-to{opacity:0;transform:translate(-80px)}.router-tab-fade-enter-active{transition:opacity .4s ease-in;will-change:opacity}.router-tab-fade-leave-active{transition:opacity .3s ease-out;will-change:opacity}.router-tab-fade-enter-from{opacity:0}.router-tab-fade-enter-to,.router-tab-fade-leave-from{opacity:1}.router-tab-fade-leave-to{opacity:0}.router-tab-scale-enter-active,.router-tab-scale-leave-active{position:relative;transition:opacity .35s ease,transform .35s cubic-bezier(.34,1.56,.64,1);will-change:opacity,transform}.router-tab-scale-enter-from{opacity:0;transform:scale(.7)}.router-tab-scale-enter-to,.router-tab-scale-leave-from{opacity:1;transform:scale(1)}.router-tab-scale-leave-to{opacity:0;transform:scale(1.2)}.router-tab-flip-enter-active,.router-tab-flip-leave-active{position:relative;transition:opacity .35s ease,transform .35s ease;transform-style:preserve-3d;will-change:opacity,transform}.router-tab-flip-enter-from{opacity:0;transform:rotateX(-90deg)}.router-tab-flip-enter-to,.router-tab-flip-leave-from{opacity:1;transform:rotateX(0)}.router-tab-flip-leave-to{opacity:0;transform:rotateX(90deg)}.router-tab-rotate-enter-active,.router-tab-rotate-leave-active{position:relative;transition:opacity .2s ease,transform .2s ease;will-change:opacity,transform}.router-tab-rotate-enter-from{opacity:0;transform:rotate(-8deg) scale(.85)}.router-tab-rotate-enter-to,.router-tab-rotate-leave-from{opacity:1;transform:rotate(0) scale(1)}.router-tab-rotate-leave-to{opacity:0;transform:rotate(8deg) scale(.85)}.router-tab-bounce-enter-active{animation:router-tab-bounce-in .5s cubic-bezier(.68,-.55,.265,1.55)}.router-tab-bounce-leave-active{animation:router-tab-bounce-out .3s ease-in}@keyframes router-tab-bounce-in{0%{opacity:0;transform:scale(.3) translateY(-80px)}50%{opacity:1;transform:scale(1.1)}70%{transform:scale(.9)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes router-tab-bounce-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.7) translateY(30px)}}
|
|
1
|
+
:root{--router-tab-header-height: 42px;--router-tab-padding: 12px;--router-tab-font-size: 14px;--router-tab-transition: all .3s ease;--router-tab-primary: #3b82f6;--router-tab-background: #ffffff;--router-tab-text: #0f172a;--router-tab-border: #e2e8f0;--router-tab-header-bg: #ffffff;--router-tab-active-background: #3b82f6;--router-tab-active-text: #ffffff;--router-tab-active-border: #3b82f6;--router-tab-icon-color: #475569;--router-tab-button-background: #f8fafc;--router-tab-button-color: #0f172a;--router-tab-active-button-background: #3b82f6;--router-tab-active-button-color: #ffffff}:root[data-theme=dark]{--router-tab-background: #1e293b;--router-tab-text: #f1f5f9;--router-tab-border: #334155;--router-tab-header-bg: #1e293b;--router-tab-icon-color: #cbd5e1;--router-tab-button-background: #1f2937;--router-tab-button-color: #f8fafc;--router-tab-active-button-background: #38bdf8;--router-tab-active-button-color: #0f172a}.router-tab{display:flex;flex-direction:column;min-height:300px;background-color:var(--router-tab-background);color:var(--router-tab-text)}.router-tab__header{position:relative;z-index:10;display:flex;flex:none;align-items:center;box-sizing:border-box;height:var(--router-tab-header-height);background-color:var(--router-tab-header-bg);border-bottom:1px solid var(--router-tab-border);transition:all .2s ease}.router-tab__scroll{position:relative;flex:1 1 0px;height:100%;overflow-x:auto;overflow-y:hidden;scroll-behavior:smooth;scrollbar-width:thin;scrollbar-color:var(--router-tab-border) transparent}.router-tab__scroll::-webkit-scrollbar{height:4px}.router-tab__scroll::-webkit-scrollbar-track{background:transparent}.router-tab__scroll::-webkit-scrollbar-thumb{background:var(--router-tab-border);border-radius:2px}.router-tab__scroll::-webkit-scrollbar-thumb:hover{background:color-mix(in srgb,var(--router-tab-primary) 50%,transparent)}.router-tab__scroll-container{width:100%;height:100%;overflow:hidden}.router-tab__scroll-container.is-mobile{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.router-tab__scroll-container.is-mobile::-webkit-scrollbar{display:none}.router-tab__nav{position:relative;display:inline-flex;flex-wrap:nowrap;height:100%;margin:0;padding:0;list-style:none}.router-tab__item{position:relative;display:flex;flex:none;align-items:center;gap:.5rem;padding:0 var(--router-tab-padding);color:var(--router-tab-text);font-size:var(--router-tab-font-size);background-color:transparent;border:none;border-right:1px solid var(--router-tab-border);cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--router-tab-transition);will-change:background-color,color}.router-tab__item:first-child{border-left:1px solid var(--router-tab-border)}.router-tab__item:hover{background-color:color-mix(in srgb,var(--router-tab-primary) 4%,transparent);color:var(--router-tab-primary)}.router-tab__item.is-active{background-color:var(--router-tab-active-background);color:var(--router-tab-active-text);font-weight:510}.router-tab__item.is-active:after{content:"";position:absolute;bottom:-1px;left:0;right:0;height:2px;background-color:var(--router-tab-active-border)}.router-tab__item.is-active+.router-tab__item{border-left-color:transparent}.router-tab__item-icon{flex-shrink:0;font-size:14px;color:var(--router-tab-icon-color)}.router-tab__item.is-active .router-tab__item-icon{color:var(--router-tab-active-text)}.router-tab__item:hover .router-tab__item-icon{color:currentColor}.router-tab__item-title{min-width:auto;max-width:180px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.router-tab__item-close{position:relative;display:block;width:0;height:12px;flex-shrink:0;overflow:hidden;border:none;border-radius:50%;background:var(--router-tab-button-background);color:var(--router-tab-button-color);cursor:pointer;transition:var(--router-tab-transition)}.router-tab__item-close:before,.router-tab__item-close:after{position:absolute;top:50%;left:50%;display:block;width:8px;height:1px;margin-left:-4px;background-color:currentColor;transition:background-color .2s ease-in-out;content:""}.router-tab__item-close:before{transform:translateY(-50%) rotate(-45deg)}.router-tab__item-close:after{transform:translateY(-50%) rotate(45deg)}.router-tab__item-close:hover{background:var(--router-tab-active-button-background);color:var(--router-tab-active-button-color)}.router-tab__item:hover .router-tab__item-close,.router-tab__item.is-active .router-tab__item-close{width:12px;margin-left:4px}.router-tab__item.is-dragging{opacity:.6;cursor:grabbing}.router-tab__item.is-drag-over{background-color:color-mix(in srgb,var(--router-tab-primary) 12%,transparent)}.router-tab__container{position:relative;flex:1 1 auto;background-color:var(--router-tab-background);overflow:hidden}.router-tab__container{padding:10px;border:1px solid var(--router-tab-border);transition:var(--router-tab-transition);will-change:background-color,border-color}.router-tab__slot-start,.router-tab__slot-end{display:none;align-items:center;gap:.25rem;padding:0 .5rem;height:100%}.router-tab__slot-start.has-content{display:flex;border-right:1px solid var(--router-tab-border)}.router-tab__slot-end.has-content{display:flex;border-left:1px solid var(--router-tab-border)}.router-tab__contextmenu{position:fixed;z-index:1000;min-width:160px;padding:6px 0;font-size:var(--router-tab-font-size);background:var(--router-tab-background);border:1px solid var(--router-tab-border);border-radius:8px;box-shadow:0 2px 8px #0000001a}.router-tab__contextmenu-item{display:block;width:100%;padding:5px 12px;line-height:32px;text-align:left;text-decoration:none;background:var(--router-tab-button-background);border:none;color:var(--router-tab-button-color);cursor:pointer;font:inherit;border-radius:4px;transition:all .2s ease-in-out;outline:none}.router-tab__contextmenu-item[aria-disabled=true]{color:color-mix(in srgb,var(--router-tab-text) 40%);pointer-events:none;cursor:not-allowed}.router-tab__contextmenu-item:focus,.router-tab__contextmenu-item:focus-visible{outline:none}.router-tab__contextmenu-item:hover:not([aria-disabled=true]),.router-tab__contextmenu-item:focus-visible,.router-tab__contextmenu-item.is-focused{background:var(--router-tab-active-button-background);color:var(--router-tab-active-button-color)}.router-tab-zoom-enter-active,.router-tab-zoom-leave-active{transition:transform all .2s ease,opacity all .2s ease}.router-tab-zoom-enter-from,.router-tab-zoom-leave-to{transform:scale(.9);opacity:0}.router-tab-zoom-enter-to,.router-tab-zoom-leave-from{transform:scale(1);opacity:1}.router-tab-zoom-move{transition:transform all .2s ease}.router-tab-swap-enter-active,.router-tab-swap-leave-active{position:relative;transition:opacity .3s ease,transform .3s ease;will-change:opacity,transform}.router-tab-swap-enter-from{opacity:0;transform:translateY(20px)}.router-tab-swap-enter-to,.router-tab-swap-leave-from{opacity:1;transform:translateY(0)}.router-tab-swap-leave-to{opacity:0;transform:translateY(-15px)}.router-tab-slide-enter-active,.router-tab-slide-leave-active{position:relative;transition:opacity .3s ease,transform .3s ease;will-change:opacity,transform}.router-tab-slide-enter-from{opacity:0;transform:translate(80px)}.router-tab-slide-enter-to,.router-tab-slide-leave-from{opacity:1;transform:translate(0)}.router-tab-slide-leave-to{opacity:0;transform:translate(-80px)}.router-tab-fade-enter-active{transition:opacity .4s ease-in;will-change:opacity}.router-tab-fade-leave-active{transition:opacity .3s ease-out;will-change:opacity}.router-tab-fade-enter-from{opacity:0}.router-tab-fade-enter-to,.router-tab-fade-leave-from{opacity:1}.router-tab-fade-leave-to{opacity:0}.router-tab-scale-enter-active,.router-tab-scale-leave-active{position:relative;transition:opacity .35s ease,transform .35s cubic-bezier(.34,1.56,.64,1);will-change:opacity,transform}.router-tab-scale-enter-from{opacity:0;transform:scale(.7)}.router-tab-scale-enter-to,.router-tab-scale-leave-from{opacity:1;transform:scale(1)}.router-tab-scale-leave-to{opacity:0;transform:scale(1.2)}.router-tab-flip-enter-active,.router-tab-flip-leave-active{position:relative;transition:opacity .35s ease,transform .35s ease;transform-style:preserve-3d;will-change:opacity,transform}.router-tab-flip-enter-from{opacity:0;transform:rotateX(-90deg)}.router-tab-flip-enter-to,.router-tab-flip-leave-from{opacity:1;transform:rotateX(0)}.router-tab-flip-leave-to{opacity:0;transform:rotateX(90deg)}.router-tab-rotate-enter-active,.router-tab-rotate-leave-active{position:relative;transition:opacity .2s ease,transform .2s ease;will-change:opacity,transform}.router-tab-rotate-enter-from{opacity:0;transform:rotate(-8deg) scale(.85)}.router-tab-rotate-enter-to,.router-tab-rotate-leave-from{opacity:1;transform:rotate(0) scale(1)}.router-tab-rotate-leave-to{opacity:0;transform:rotate(8deg) scale(.85)}.router-tab-bounce-enter-active{animation:router-tab-bounce-in .5s cubic-bezier(.68,-.55,.265,1.55)}.router-tab-bounce-leave-active{animation:router-tab-bounce-out .3s ease-in}@keyframes router-tab-bounce-in{0%{opacity:0;transform:scale(.3) translateY(-80px)}50%{opacity:1;transform:scale(1.1)}70%{transform:scale(.9)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes router-tab-bounce-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.7) translateY(30px)}}
|