vue3-router-tab 1.2.5 → 1.2.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
@@ -28,7 +28,7 @@ app.mount('#app')
28
28
 
29
29
  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.
30
30
 
31
- ## Basic usage
31
+ ## Basic Usage
32
32
 
33
33
  ```vue
34
34
  <template>
@@ -38,6 +38,29 @@ The plugin registers the `<router-tab>` component globally. It also exposes an o
38
38
 
39
39
  > Hint: `cookie-key` is optional. Omit it to fall back to the default `router-tabs:snapshot` cookie, or set your own as shown above.
40
40
 
41
+ ### Enhanced Usage
42
+
43
+ ```vue
44
+ <template>
45
+ <router-tab
46
+ cookie-key="app-tabs"
47
+ :sortable="true"
48
+ @tab-sort="onTabSort"
49
+ @tab-sorted="onTabSorted"
50
+ />
51
+ </template>
52
+
53
+ <script setup>
54
+ function onTabSort({ tab, index }) {
55
+ console.log('Tab drag started:', tab.title, 'at index', index)
56
+ }
57
+
58
+ function onTabSorted({ tab, fromIndex, toIndex }) {
59
+ console.log('Tab moved:', tab.title, 'from', fromIndex, 'to', toIndex)
60
+ }
61
+ </script>
62
+ ```
63
+
41
64
  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:
42
65
 
43
66
  ```vue
@@ -79,6 +102,165 @@ const routes = [
79
102
 
80
103
  `meta.key` accepts the shortcuts `fullPath`, `path`, or `name`, or you can supply your own function.
81
104
 
105
+ ## Configuration
106
+
107
+ ### RouterTab Props
108
+
109
+ | Prop | Type | Default | Description |
110
+ |------|------|---------|-------------|
111
+ | `tabs` | `TabInput[]` | `[]` | Initial tabs to display |
112
+ | `keepAlive` | `boolean` | `true` | Enable keep-alive for tab components |
113
+ | `maxAlive` | `number` | `0` | Maximum number of alive components (0 = unlimited) |
114
+ | `keepLastTab` | `boolean` | `true` | Prevent closing the last remaining tab |
115
+ | `append` | `'last' \| 'next'` | `'last'` | Where to append new tabs |
116
+ | `defaultPage` | `string \| object` | `'/'` | Default route to navigate to |
117
+ | `tabTransition` | `string \| object` | `'router-tab-zoom'` | Transition for tab changes |
118
+ | `pageTransition` | `string \| object` | `{ name: 'router-tab-swap', mode: 'out-in' }` | Transition for page changes |
119
+ | `contextmenu` | `boolean \| array` | `true` | Enable context menu or provide custom menu items |
120
+ | `cookieKey` | `string` | `'router-tabs:snapshot'` | Cookie key for persistence |
121
+ | `persistence` | `object \| null` | `null` | Persistence configuration |
122
+ | `sortable` | `boolean` | `true` | Enable drag-and-drop tab sorting |
123
+
124
+ ### Reactive Tab Updates
125
+
126
+ RouterTab automatically watches for reactive properties in your page components and updates the corresponding tab information in real-time. The tab titles, icons, and other properties displayed in the tab bar will automatically update when the reactive properties in your components change.
127
+
128
+ #### Watched Properties
129
+
130
+ The following reactive properties in your page components are automatically monitored:
131
+
132
+ - **`routeTabTitle`** - Updates the tab title
133
+ - **`routeTabIcon`** - Updates the tab icon
134
+ - **`routeTabClosable`** - Updates whether the tab can be closed
135
+ - **`routeTabMeta`** - Updates additional tab metadata
136
+
137
+ #### Basic Usage
138
+
139
+ ```vue
140
+ <template>
141
+ <div>
142
+ <button @click="updateTitle">Update Title</button>
143
+ <button @click="toggleLoading">Toggle Loading</button>
144
+ </div>
145
+ </template>
146
+
147
+ <script setup>
148
+ import { ref, computed } from 'vue'
149
+
150
+ // Simple reactive title - tab updates immediately when this changes
151
+ const routeTabTitle = ref('My Page')
152
+
153
+ // Dynamic title based on state
154
+ const isLoading = ref(false)
155
+ const routeTabTitle = computed(() =>
156
+ isLoading.value ? 'Loading...' : 'My Page'
157
+ )
158
+
159
+ // Dynamic icon - tab icon updates automatically
160
+ const routeTabIcon = computed(() =>
161
+ isLoading.value ? 'mdi-loading mdi-spin' : 'mdi-page'
162
+ )
163
+
164
+ // Conditional closability - tab close button appears/disappears automatically
165
+ const routeTabClosable = computed(() => !isLoading.value)
166
+
167
+ // Functions that trigger reactive updates
168
+ function updateTitle() {
169
+ routeTabTitle.value = `Updated ${Date.now()}`
170
+ }
171
+
172
+ function toggleLoading() {
173
+ isLoading.value = !isLoading.value
174
+ }
175
+ </script>
176
+ ```
177
+
178
+ #### Advanced Examples
179
+
180
+ ```vue
181
+ <script setup>
182
+ import { ref, computed } from 'vue'
183
+
184
+ const notifications = ref(0)
185
+ const hasError = ref(false)
186
+ const isProcessing = ref(false)
187
+
188
+ // Dynamic title with notification count
189
+ const routeTabTitle = computed(() => {
190
+ if (hasError.value) return 'Error!'
191
+ if (isProcessing.value) return 'Processing...'
192
+ if (notifications.value > 0) return `Messages (${notifications.value})`
193
+ return 'Dashboard'
194
+ })
195
+
196
+ // State-based icons
197
+ const routeTabIcon = computed(() => {
198
+ if (hasError.value) return 'mdi-alert'
199
+ if (isProcessing.value) return 'mdi-loading mdi-spin'
200
+ if (notifications.value > 0) return 'mdi-bell-badge'
201
+ return 'mdi-view-dashboard'
202
+ })
203
+
204
+ // Prevent closing during critical operations
205
+ const routeTabClosable = computed(() => !isProcessing.value)
206
+
207
+ // Custom metadata for advanced use cases
208
+ const routeTabMeta = computed(() => ({
209
+ status: hasError.value ? 'error' : 'normal',
210
+ count: notifications.value,
211
+ timestamp: Date.now()
212
+ }))
213
+ </script>
214
+ ```
215
+
216
+ #### Using Composables
217
+
218
+ For easier reactive tab management, use the provided composables:
219
+
220
+ ```vue
221
+ <script setup>
222
+ import {
223
+ useReactiveTab,
224
+ useLoadingTab,
225
+ useNotificationTab,
226
+ useStatusTab
227
+ } from 'vue3-router-tab'
228
+
229
+ // Basic composable
230
+ const { routeTabTitle, routeTabIcon, updateTitle } = useReactiveTab({
231
+ title: 'My Page',
232
+ icon: 'mdi-page'
233
+ })
234
+
235
+ // Loading state preset
236
+ const isLoading = ref(false)
237
+ const loadingTab = useLoadingTab(isLoading, 'Dashboard')
238
+
239
+ // Notification preset
240
+ const count = ref(0)
241
+ const notificationTab = useNotificationTab(count, 'Messages')
242
+
243
+ // Status preset
244
+ const status = ref('normal')
245
+ const statusTab = useStatusTab(status, 'Process')
246
+ </script>
247
+ ```
248
+
249
+ **Available Composables:**
250
+ - `useReactiveTab(config)` - Basic reactive tab management
251
+ - `useLoadingTab(loading, baseTitle)` - Loading state management
252
+ - `useNotificationTab(count, baseTitle, baseIcon)` - Notification badges
253
+ - `useStatusTab(status, baseTitle)` - Status-based updates
254
+
255
+ ### Tab Closing Behavior
256
+
257
+ When a tab is closed, the plugin automatically navigates to the next available tab with the following priority:
258
+
259
+ 1. **Next tab** - The tab immediately to the right
260
+ 2. **Previous tab** - The tab immediately to the left
261
+ 3. **First tab** - If no adjacent tabs exist
262
+ 4. **Default route** - If no other tabs exist
263
+
82
264
  ## Cookie persistence
83
265
 
84
266
  `<router-tab cookie-key="…" />` is usually all you need. If you prefer fine grained control (custom expiry, same-site, etc.) you can still use the headless helper:
@@ -152,7 +334,39 @@ You can override the default routed view by providing a `#default` slot. The slo
152
334
  </router-tab>
153
335
  ```
154
336
 
155
- ## Customising the context menu
337
+ ## Advanced Features
338
+
339
+ ### Tab Sorting
340
+
341
+ Enable drag-and-drop tab reordering with the `sortable` prop:
342
+
343
+ ```vue
344
+ <router-tab
345
+ :sortable="true"
346
+ @tab-sort="onTabSort"
347
+ @tab-sorted="onTabSorted"
348
+ />
349
+ ```
350
+
351
+
352
+
353
+ ### Persistence Options
354
+
355
+ Fine-grained control over tab persistence:
356
+
357
+ ```vue
358
+ <router-tab
359
+ :persistence="{
360
+ cookieKey: 'my-app-tabs',
361
+ expiresInDays: 30,
362
+ fallbackRoute: '/dashboard',
363
+ serialize: (snapshot) => btoa(JSON.stringify(snapshot)),
364
+ deserialize: (data) => JSON.parse(atob(data))
365
+ }"
366
+ />
367
+ ```
368
+
369
+ ## Customising the Context Menu
156
370
 
157
371
  ```vue
158
372
  <router-tab
@@ -171,6 +385,15 @@ You can override the default routed view by providing a `#default` slot. The slo
171
385
 
172
386
  Pass `false` to disable the context menu entirely.
173
387
 
388
+ ### Built-in Menu Items
389
+
390
+ - `refresh` - Refresh current tab
391
+ - `refreshAll` - Refresh all tabs
392
+ - `close` - Close current tab
393
+ - `closeLefts` - Close tabs to the left
394
+ - `closeRights` - Close tabs to the right
395
+ - `closeOthers` - Close all other tabs
396
+
174
397
  ## Slots
175
398
 
176
399
  - `start` / `end` – positioned on either side of the tab list (ideal for toolbars or the `<router-tabs>` helper).
@@ -1 +1 @@
1
- :root{--router-tab-header-height: 42px;--router-tab-padding: 12px;--router-tab-font-size: 14px}.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)}.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:60px;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-bg);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-bg);color:var(--router-tab-active-button-color)}.router-tab__item:hover.is-closable,.router-tab__item.is-active.is-closable{padding-right:calc(var(--router-tab-padding) - 8px)}.router-tab__item:hover .router-tab__item-close,.router-tab__item.is-active .router-tab__item-close{width:12px;margin-left:4px}.router-tab__container{position:relative;flex:1 1 auto;background-color:var(--router-tab-background);overflow:hidden}.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:0 12px;line-height:32px;text-align:left;text-decoration:none;background:var(--router-tab-button-bg);border:none;color:var(--router-tab-button-color);cursor:pointer;font:inherit;border-radius:4px;transition:all .2s ease-in-out}.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:hover:not([aria-disabled=true]),.router-tab__contextmenu-item:focus-visible{background:var(--router-tab-active-button-bg);color:var(--router-tab-active-button-color)}
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{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)}.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:60px;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-bg);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-bg);color:var(--router-tab-active-button-color)}.router-tab__item:hover.is-closable,.router-tab__item.is-active.is-closable{padding-right:calc(var(--router-tab-padding) - 8px)}.router-tab__item:hover .router-tab__item-close,.router-tab__item.is-active .router-tab__item-close{width:12px;margin-left:4px}.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)}.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:0 12px;line-height:32px;text-align:left;text-decoration:none;background:var(--router-tab-button-bg);border:none;color:var(--router-tab-button-color);cursor:pointer;font:inherit;border-radius:4px;transition:all .2s ease-in-out}.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:hover:not([aria-disabled=true]),.router-tab__contextmenu-item:focus-visible{background:var(--router-tab-active-button-bg);color:var(--router-tab-active-button-color)}