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 CHANGED
@@ -1,36 +1,46 @@
1
- # vue3-router-tab
1
+ # Vue3 Router Tab
2
2
 
3
- A Vue 3 tab-bar plugin that keeps multiple routes alive with transition support, context menus, and optional cookie-based persistence.
3
+ [![npm version](https://badge.fury.io/js/vue3-router-tab.svg)](https://badge.fury.io/js/vue3-router-tab)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
4
6
 
5
- ## ✨ Features
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
- - 🎯 **Multi-tab Navigation** - Keep multiple routes alive simultaneously
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
- - πŸŽ›οΈ **Highly Configurable** - Extensive props and events for customization
16
- - πŸ“± **TypeScript Support** - Full TypeScript definitions included
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
- ## Register the plugin
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
- The plugin registers the `<router-tab>` component globally. It also exposes an optional `<router-tabs>` helper for advanced cookie options, but you rarely need it now that persistence can be enabled directly on the tab component.
43
-
44
- ## Basic Usage
52
+ ### 2. Basic Usage
45
53
 
46
54
  ```vue
47
55
  <template>
48
- <router-tab cookie-key="app-tabs" />
56
+ <router-tab />
49
57
  </template>
50
58
  ```
51
59
 
52
- > Hint: `cookie-key` is optional. Omit it to fall back to the default `router-tabs:snapshot` cookie, or set your own as shown above.
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
- @tab-sort="onTabSort"
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
- Need to customise the rendered output? Provide a slot and use the routed component directlyβ€”see [`example-app/src/App.vue`](example-app/src/App.vue) for a working sample:
74
+ ## πŸ“– Usage Guide
75
+
76
+ ### Basic Configuration
78
77
 
79
78
  ```vue
80
- <router-tab cookie-key="app-tabs">
81
- <template #default="{ Component, route }">
82
- <Suspense>
83
- <component :is="Component" :key="route.fullPath" />
84
- </Suspense>
85
- </template>
86
- </router-tab>
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
- Configure route metadata to control tab labels, icons, and lifecycle behaviour:
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: 'fa fa-home',
99
- key: 'fullPath',
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: '/about',
106
- component: About,
120
+ path: '/settings',
121
+ component: Settings,
107
122
  meta: {
108
- title: 'About',
109
- icon: 'fa fa-info-circle',
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
@@ -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)}}