vue3-router-tab 1.2.5 → 1.2.7
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 +225 -2
- package/dist/vue3-router-tab.css +1 -1
- package/dist/vue3-router-tab.js +767 -463
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +367 -22
- package/lib/core/createRouterTabs.ts +31 -9
- package/lib/index.ts +52 -9
- package/lib/persistence.ts +3 -1
- package/lib/scss/index.scss +73 -9
- package/lib/scss/variables.scss +14 -2
- package/lib/theme.ts +41 -5
- package/lib/useReactiveTab.ts +130 -0
- package/package.json +8 -3
|
@@ -106,9 +106,9 @@ function insertTab(tabs: TabRecord[], tab: TabRecord, position: 'last' | 'next',
|
|
|
106
106
|
if (exists) return
|
|
107
107
|
|
|
108
108
|
if (position === 'next' && referenceId) {
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
tabs.splice(
|
|
109
|
+
const referenceIndex = tabs.findIndex(item => item.id === referenceId)
|
|
110
|
+
if (referenceIndex !== -1) {
|
|
111
|
+
tabs.splice(referenceIndex + 1, 0, tab)
|
|
112
112
|
return
|
|
113
113
|
}
|
|
114
114
|
}
|
|
@@ -211,7 +211,14 @@ export function createRouterTabs(
|
|
|
211
211
|
|
|
212
212
|
function fallbackAfterClose(closedId: string): RouteLocationRaw | null {
|
|
213
213
|
const idx = tabs.findIndex(item => item.id === closedId)
|
|
214
|
-
|
|
214
|
+
if (idx === -1) return options.defaultRoute
|
|
215
|
+
|
|
216
|
+
// Priority: next tab -> previous tab -> first available tab
|
|
217
|
+
const nextTab = tabs[idx + 1] // Next tab (after the one being closed)
|
|
218
|
+
const prevTab = tabs[idx - 1] // Previous tab
|
|
219
|
+
const firstTab = tabs.find(tab => tab.id !== closedId) // First available tab (excluding the one being closed)
|
|
220
|
+
|
|
221
|
+
const candidate = nextTab || prevTab || firstTab
|
|
215
222
|
if (candidate) return candidate.to
|
|
216
223
|
return options.defaultRoute
|
|
217
224
|
}
|
|
@@ -222,15 +229,20 @@ export function createRouterTabs(
|
|
|
222
229
|
throw new Error('[RouterTabs] Unable to close the final tab when keepLastTab is true.')
|
|
223
230
|
}
|
|
224
231
|
|
|
232
|
+
// Calculate fallback route BEFORE removing the tab
|
|
233
|
+
const isClosingActiveTab = activeId.value === id
|
|
234
|
+
const shouldRedirect = isClosingActiveTab && closeOptions.redirect !== null
|
|
235
|
+
const fallbackRoute = shouldRedirect ? (closeOptions.redirect ?? fallbackAfterClose(id)) : null
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
225
239
|
await removeTab(id, { force: closeOptions.force })
|
|
226
240
|
|
|
241
|
+
// Only skip redirect if explicitly set to null
|
|
227
242
|
if (closeOptions.redirect === null) return
|
|
228
243
|
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
if (redirect) await router.replace(redirect)
|
|
232
|
-
} else if (closeOptions.redirect) {
|
|
233
|
-
await router.replace(closeOptions.redirect)
|
|
244
|
+
if (shouldRedirect && fallbackRoute) {
|
|
245
|
+
await router.replace(fallbackRoute)
|
|
234
246
|
}
|
|
235
247
|
}
|
|
236
248
|
|
|
@@ -255,6 +267,16 @@ export function createRouterTabs(
|
|
|
255
267
|
|
|
256
268
|
async function refreshTab(id: string | undefined = activeId.value ?? undefined, force = false) {
|
|
257
269
|
if (!id) return
|
|
270
|
+
const tab = tabs.find(item => item.id === id)
|
|
271
|
+
if (!tab) return
|
|
272
|
+
|
|
273
|
+
if (options.keepAlive && tab.alive) {
|
|
274
|
+
tab.alive = false
|
|
275
|
+
await nextTick()
|
|
276
|
+
tab.alive = true
|
|
277
|
+
await nextTick()
|
|
278
|
+
}
|
|
279
|
+
|
|
258
280
|
refreshingKey.value = id
|
|
259
281
|
await nextTick()
|
|
260
282
|
if (!force) await nextTick()
|
package/lib/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from './theme'
|
|
12
12
|
|
|
13
13
|
import type { RouterTabsContext } from './core/types'
|
|
14
|
+
import type { RouterTabsThemeOptions } from './theme'
|
|
14
15
|
|
|
15
16
|
export type {
|
|
16
17
|
TabRecord,
|
|
@@ -22,6 +23,28 @@ export type {
|
|
|
22
23
|
|
|
23
24
|
export type { RouterTabsThemeOptions } from './theme'
|
|
24
25
|
|
|
26
|
+
export interface RouterTabsPluginOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Whether to initialise the theme system automatically during install.
|
|
29
|
+
* Defaults to `true`.
|
|
30
|
+
*/
|
|
31
|
+
initTheme?: boolean
|
|
32
|
+
/**
|
|
33
|
+
* Theme options passed to `initRouterTabsTheme` when `initTheme` is enabled.
|
|
34
|
+
*/
|
|
35
|
+
themeOptions?: RouterTabsThemeOptions
|
|
36
|
+
/**
|
|
37
|
+
* Global component name used when registering `RouterTab`.
|
|
38
|
+
* Defaults to the component's `name` option or `"RouterTab"`.
|
|
39
|
+
*/
|
|
40
|
+
componentName?: string
|
|
41
|
+
/**
|
|
42
|
+
* Global component name used when registering `RouterTabs`.
|
|
43
|
+
* Defaults to the component's `name` option or `"RouterTabs"`.
|
|
44
|
+
*/
|
|
45
|
+
tabsComponentName?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
export {
|
|
26
49
|
routerTabsKey,
|
|
27
50
|
useRouterTabs,
|
|
@@ -33,22 +56,42 @@ export {
|
|
|
33
56
|
setRouterTabsPrimary
|
|
34
57
|
}
|
|
35
58
|
|
|
36
|
-
|
|
59
|
+
export {
|
|
60
|
+
useReactiveTab,
|
|
61
|
+
useLoadingTab,
|
|
62
|
+
useNotificationTab,
|
|
63
|
+
useStatusTab
|
|
64
|
+
} from './useReactiveTab'
|
|
65
|
+
|
|
66
|
+
export type {
|
|
67
|
+
ReactiveTabState,
|
|
68
|
+
ReactiveTabReturn
|
|
69
|
+
} from './useReactiveTab'
|
|
70
|
+
|
|
71
|
+
import './scss/index.scss'
|
|
72
|
+
|
|
73
|
+
let installed = false
|
|
37
74
|
|
|
38
75
|
const plugin: Plugin = {
|
|
39
|
-
install(app: App) {
|
|
40
|
-
if (
|
|
41
|
-
|
|
76
|
+
install(app: App, options?: RouterTabsPluginOptions) {
|
|
77
|
+
if (installed) return
|
|
78
|
+
installed = true
|
|
42
79
|
|
|
43
|
-
|
|
80
|
+
const {
|
|
81
|
+
initTheme = true,
|
|
82
|
+
themeOptions,
|
|
83
|
+
componentName = RouterTab.name || 'RouterTab',
|
|
84
|
+
tabsComponentName = RouterTabsComponent.name || 'RouterTabs'
|
|
85
|
+
} = options ?? {}
|
|
44
86
|
|
|
45
|
-
|
|
46
|
-
|
|
87
|
+
if (initTheme) {
|
|
88
|
+
initRouterTabsTheme(themeOptions ?? {})
|
|
89
|
+
}
|
|
47
90
|
|
|
48
91
|
app.component(componentName, RouterTab)
|
|
49
|
-
app.component(
|
|
92
|
+
app.component(tabsComponentName, RouterTabsComponent)
|
|
50
93
|
|
|
51
|
-
if (
|
|
94
|
+
if (tabsComponentName.toLowerCase() !== 'router-tabs') {
|
|
52
95
|
app.component('router-tabs', RouterTabsComponent)
|
|
53
96
|
}
|
|
54
97
|
|
package/lib/persistence.ts
CHANGED
|
@@ -119,7 +119,7 @@ export function useRouterTabsPersistence(options: RouterTabsPersistenceOptions =
|
|
|
119
119
|
} finally {
|
|
120
120
|
hydrating.value = false
|
|
121
121
|
}
|
|
122
|
-
} else {
|
|
122
|
+
} else if (Object.prototype.hasOwnProperty.call(options, 'fallbackRoute')) {
|
|
123
123
|
try {
|
|
124
124
|
hydrating.value = true
|
|
125
125
|
const fallback = options.fallbackRoute ?? ctrl.options.defaultRoute
|
|
@@ -127,6 +127,8 @@ export function useRouterTabsPersistence(options: RouterTabsPersistenceOptions =
|
|
|
127
127
|
} finally {
|
|
128
128
|
hydrating.value = false
|
|
129
129
|
}
|
|
130
|
+
} else {
|
|
131
|
+
hydrating.value = false
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
const snapshot = ctrl.snapshot()
|
package/lib/scss/index.scss
CHANGED
|
@@ -12,6 +12,34 @@
|
|
|
12
12
|
--router-tab-header-height: #{$router-tab-header-height};
|
|
13
13
|
--router-tab-padding: #{$router-tab-padding};
|
|
14
14
|
--router-tab-font-size: #{$router-tab-font-size};
|
|
15
|
+
--router-tab-transition: #{$router-tab-transition};
|
|
16
|
+
|
|
17
|
+
// Colors (will be overridden by theme system)
|
|
18
|
+
--router-tab-primary: #{$router-tab-primary};
|
|
19
|
+
--router-tab-background: #{$router-tab-bg-light};
|
|
20
|
+
--router-tab-text: #{$router-tab-text-light};
|
|
21
|
+
--router-tab-border: #{$router-tab-border-light};
|
|
22
|
+
--router-tab-header-bg: #{$router-tab-bg-light};
|
|
23
|
+
--router-tab-active-background: #{$router-tab-primary};
|
|
24
|
+
--router-tab-active-text: #ffffff;
|
|
25
|
+
--router-tab-active-border: #{$router-tab-primary};
|
|
26
|
+
--router-tab-icon-color: #{$router-tab-icon-color-light};
|
|
27
|
+
--router-tab-button-background: #{$router-tab-button-bg-light};
|
|
28
|
+
--router-tab-button-color: #{$router-tab-button-color-light};
|
|
29
|
+
--router-tab-active-button-background: #{$router-tab-active-button-bg-light};
|
|
30
|
+
--router-tab-active-button-color: #{$router-tab-active-button-color-light};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:root[data-theme="dark"] {
|
|
34
|
+
--router-tab-background: #{$router-tab-bg-dark};
|
|
35
|
+
--router-tab-text: #{$router-tab-text-dark};
|
|
36
|
+
--router-tab-border: #{$router-tab-border-dark};
|
|
37
|
+
--router-tab-header-bg: #{$router-tab-bg-dark};
|
|
38
|
+
--router-tab-icon-color: #{$router-tab-icon-color-dark};
|
|
39
|
+
--router-tab-button-background: #{$router-tab-button-bg-dark};
|
|
40
|
+
--router-tab-button-color: #{$router-tab-button-color-dark};
|
|
41
|
+
--router-tab-active-button-background: #{$router-tab-active-button-bg-dark};
|
|
42
|
+
--router-tab-active-button-color: #{$router-tab-active-button-color-dark};
|
|
15
43
|
}
|
|
16
44
|
|
|
17
45
|
// =============================================================================
|
|
@@ -87,6 +115,7 @@
|
|
|
87
115
|
cursor: pointer;
|
|
88
116
|
user-select: none;
|
|
89
117
|
transition: var(--router-tab-transition);
|
|
118
|
+
will-change: background-color, color;
|
|
90
119
|
|
|
91
120
|
&:first-child {
|
|
92
121
|
border-left: 1px solid var(--router-tab-border);
|
|
@@ -153,7 +182,7 @@
|
|
|
153
182
|
overflow: hidden;
|
|
154
183
|
border: none;
|
|
155
184
|
border-radius: 50%;
|
|
156
|
-
background: var(--router-tab-button-
|
|
185
|
+
background: var(--router-tab-button-background);
|
|
157
186
|
color: var(--router-tab-button-color);
|
|
158
187
|
cursor: pointer;
|
|
159
188
|
transition: var(--router-tab-transition);
|
|
@@ -176,7 +205,7 @@
|
|
|
176
205
|
&::after { transform: translateY(-50%) rotate(45deg); }
|
|
177
206
|
|
|
178
207
|
&:hover {
|
|
179
|
-
background: var(--router-tab-active-button-
|
|
208
|
+
background: var(--router-tab-active-button-background);
|
|
180
209
|
color: var(--router-tab-active-button-color);
|
|
181
210
|
}
|
|
182
211
|
}
|
|
@@ -184,15 +213,21 @@
|
|
|
184
213
|
// Show close button on hover/active
|
|
185
214
|
&:hover,
|
|
186
215
|
&.is-active {
|
|
187
|
-
&.is-closable {
|
|
188
|
-
padding-right: calc(var(--router-tab-padding) - #{($router-tab-close-icon-size + $router-tab-close-icon-margin) * 0.5});
|
|
189
|
-
}
|
|
190
216
|
|
|
191
217
|
.router-tab__item-close {
|
|
192
218
|
width: $router-tab-close-icon-size;
|
|
193
219
|
margin-left: $router-tab-close-icon-margin;
|
|
194
220
|
}
|
|
195
221
|
}
|
|
222
|
+
|
|
223
|
+
&.is-dragging {
|
|
224
|
+
opacity: $router-tab-drag-opacity;
|
|
225
|
+
cursor: $router-tab-drag-cursor;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
&.is-drag-over {
|
|
229
|
+
background-color: color-mix(in srgb, var(--router-tab-primary) 12%, transparent);
|
|
230
|
+
}
|
|
196
231
|
}
|
|
197
232
|
|
|
198
233
|
&__container {
|
|
@@ -205,7 +240,29 @@
|
|
|
205
240
|
.router-tab__container {
|
|
206
241
|
padding: 10px;
|
|
207
242
|
border: 1px solid var(--router-tab-border);
|
|
243
|
+
transition: var(--router-tab-transition);
|
|
244
|
+
will-change: background-color, border-color;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.router-tab__slot-start,
|
|
248
|
+
.router-tab__slot-end {
|
|
249
|
+
display: none;
|
|
250
|
+
align-items: center;
|
|
251
|
+
gap: 0.25rem;
|
|
252
|
+
padding: 0 0.5rem;
|
|
253
|
+
height: 100%;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.router-tab__slot-start.has-content {
|
|
257
|
+
display: flex;
|
|
258
|
+
border-right: 1px solid var(--router-tab-border);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.router-tab__slot-end.has-content {
|
|
262
|
+
display: flex;
|
|
263
|
+
border-left: 1px solid var(--router-tab-border);
|
|
208
264
|
}
|
|
265
|
+
|
|
209
266
|
// Context Menu using button colors
|
|
210
267
|
.router-tab__contextmenu {
|
|
211
268
|
position: fixed;
|
|
@@ -225,13 +282,14 @@
|
|
|
225
282
|
line-height: $router-tab-contextmenu-item-height;
|
|
226
283
|
text-align: left;
|
|
227
284
|
text-decoration: none;
|
|
228
|
-
background: var(--router-tab-button-
|
|
285
|
+
background: var(--router-tab-button-background);
|
|
229
286
|
border: none;
|
|
230
287
|
color: var(--router-tab-button-color);
|
|
231
288
|
cursor: pointer;
|
|
232
289
|
font: inherit;
|
|
233
290
|
border-radius: calc(#{$router-tab-contextmenu-border-radius} - 4px);
|
|
234
291
|
transition: all 0.2s ease-in-out;
|
|
292
|
+
outline: none;
|
|
235
293
|
|
|
236
294
|
&[aria-disabled="true"] {
|
|
237
295
|
color: color-mix(in srgb, var(--router-tab-text) 40%);
|
|
@@ -239,10 +297,16 @@
|
|
|
239
297
|
cursor: not-allowed;
|
|
240
298
|
}
|
|
241
299
|
|
|
242
|
-
&:
|
|
300
|
+
&:focus,
|
|
243
301
|
&:focus-visible {
|
|
244
|
-
|
|
302
|
+
outline: none;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
&:hover:not([aria-disabled="true"]),
|
|
306
|
+
&:focus-visible,
|
|
307
|
+
&.is-focused {
|
|
308
|
+
background: var(--router-tab-active-button-background);
|
|
245
309
|
color: var(--router-tab-active-button-color);
|
|
246
310
|
}
|
|
247
311
|
}
|
|
248
|
-
}
|
|
312
|
+
}
|
package/lib/scss/variables.scss
CHANGED
|
@@ -18,12 +18,24 @@ $router-tab-padding: 12px !default;
|
|
|
18
18
|
$router-tab-font-size: 14px !default;
|
|
19
19
|
|
|
20
20
|
// Title
|
|
21
|
-
$router-tab-title-min-width:
|
|
21
|
+
$router-tab-title-min-width: auto !default;
|
|
22
22
|
$router-tab-title-max-width: 180px !default;
|
|
23
23
|
|
|
24
24
|
// Icons
|
|
25
25
|
$router-tab-icon-size: 14px !default;
|
|
26
26
|
$router-tab-icon-margin: 6px !default;
|
|
27
|
+
$router-tab-icon-color-light: #475569 !default;
|
|
28
|
+
$router-tab-icon-color-dark: #cbd5e1 !default;
|
|
29
|
+
|
|
30
|
+
// Buttons
|
|
31
|
+
$router-tab-button-bg-light: #f8fafc !default;
|
|
32
|
+
$router-tab-button-bg-dark: #1f2937 !default;
|
|
33
|
+
$router-tab-button-color-light: #0f172a !default;
|
|
34
|
+
$router-tab-button-color-dark: #f8fafc !default;
|
|
35
|
+
$router-tab-active-button-bg-light: $router-tab-primary !default;
|
|
36
|
+
$router-tab-active-button-bg-dark: #38bdf8 !default;
|
|
37
|
+
$router-tab-active-button-color-light: #ffffff !default;
|
|
38
|
+
$router-tab-active-button-color-dark: #0f172a !default;
|
|
27
39
|
|
|
28
40
|
// Close icon
|
|
29
41
|
$router-tab-close-icon-size: 12px !default;
|
|
@@ -33,7 +45,7 @@ $router-tab-close-icon-margin: 4px !default;
|
|
|
33
45
|
$router-tab-contextmenu-min-width: 160px !default;
|
|
34
46
|
$router-tab-contextmenu-padding: 6px 0 !default;
|
|
35
47
|
$router-tab-contextmenu-item-height: 32px !default;
|
|
36
|
-
$router-tab-contextmenu-item-padding:
|
|
48
|
+
$router-tab-contextmenu-item-padding: 5px 12px !default;
|
|
37
49
|
$router-tab-contextmenu-border-radius: 8px !default;
|
|
38
50
|
$router-tab-contextmenu-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !default;
|
|
39
51
|
|
package/lib/theme.ts
CHANGED
|
@@ -46,7 +46,7 @@ const defaultColors: ColorStyle = {
|
|
|
46
46
|
activeText: "#ffffff",
|
|
47
47
|
activeBorder: "#034960",
|
|
48
48
|
|
|
49
|
-
headerBackground: "#
|
|
49
|
+
headerBackground: "#ffffff",
|
|
50
50
|
|
|
51
51
|
buttonBackground: "#f8fafc",
|
|
52
52
|
buttonColor: "#034960",
|
|
@@ -76,6 +76,22 @@ const defaultDarkColor: ColorStyle = {
|
|
|
76
76
|
iconColor: "#cbd5e1",
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function readStoredPrimary(key: string): Partial<ColorStyle> | null {
|
|
80
|
+
if (typeof window === 'undefined') return null
|
|
81
|
+
const raw = window.localStorage.getItem(key)
|
|
82
|
+
if (!raw) return null
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(raw) as Partial<ColorStyle>
|
|
86
|
+
return parsed && typeof parsed === 'object' ? parsed : null
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (import.meta.env?.DEV) {
|
|
89
|
+
console.warn('[RouterTabs] Failed to parse stored primary color palette', error)
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
function applyPrimary(color: ColorStyle) {
|
|
80
96
|
if (typeof document === 'undefined') return
|
|
81
97
|
document.documentElement.style.setProperty('--router-tab-primary', color.primary ?? defaultColors.primary)
|
|
@@ -90,6 +106,7 @@ function applyPrimary(color: ColorStyle) {
|
|
|
90
106
|
document.documentElement.style.setProperty('--router-tab-active-button-color', color.activeButtonColor ?? defaultColors.activeButtonColor)
|
|
91
107
|
document.documentElement.style.setProperty('--router-tab-button-background', color.buttonBackground ?? defaultColors.buttonBackground)
|
|
92
108
|
document.documentElement.style.setProperty('--router-tab-active-button-background', color.activeButtonBackground ?? defaultColors.activeButtonBackground)
|
|
109
|
+
document.documentElement.style.setProperty('--router-tab-icon-color', color.iconColor ?? defaultColors.iconColor)
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
function applyStyle(style: 'light' | 'dark' | 'system') {
|
|
@@ -121,17 +138,36 @@ export function initRouterTabsTheme(options: RouterTabsThemeOptions = {}) {
|
|
|
121
138
|
|
|
122
139
|
const {
|
|
123
140
|
styleKey = STYLE_KEY,
|
|
141
|
+
primaryKey = PRIMARY_KEY,
|
|
124
142
|
defaultStyle = DEFAULT_STYLE,
|
|
143
|
+
defaultPrimary
|
|
125
144
|
} = options
|
|
126
145
|
|
|
127
146
|
const storedStyle = (window.localStorage.getItem(styleKey) as 'light' | 'dark' | 'system' | null) ?? defaultStyle
|
|
128
147
|
|
|
129
148
|
applyStyle(storedStyle)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
|
|
150
|
+
const prefersDark =
|
|
151
|
+
storedStyle === 'dark' ||
|
|
152
|
+
(storedStyle === 'system' && window.matchMedia(MEDIA_QUERY).matches)
|
|
153
|
+
|
|
154
|
+
const basePalette = prefersDark
|
|
155
|
+
? { ...defaultDarkColor }
|
|
156
|
+
: { ...defaultColors }
|
|
157
|
+
|
|
158
|
+
if (defaultPrimary) {
|
|
159
|
+
basePalette.primary = defaultPrimary
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const storedPrimary = readStoredPrimary(primaryKey)
|
|
163
|
+
|
|
164
|
+
if (storedPrimary) {
|
|
165
|
+
applyPrimary({
|
|
166
|
+
...basePalette,
|
|
167
|
+
...storedPrimary
|
|
168
|
+
})
|
|
133
169
|
} else {
|
|
134
|
-
applyPrimary(
|
|
170
|
+
applyPrimary(basePalette)
|
|
135
171
|
}
|
|
136
172
|
}
|
|
137
173
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ref,
|
|
3
|
+
computed,
|
|
4
|
+
isRef,
|
|
5
|
+
isReadonly,
|
|
6
|
+
type Ref,
|
|
7
|
+
type ComputedRef
|
|
8
|
+
} from 'vue'
|
|
9
|
+
|
|
10
|
+
export interface ReactiveTabState {
|
|
11
|
+
title?: string | Ref<string> | ComputedRef<string>
|
|
12
|
+
icon?: string | Ref<string> | ComputedRef<string>
|
|
13
|
+
closable?: boolean | Ref<boolean> | ComputedRef<boolean>
|
|
14
|
+
meta?: any | Ref<any> | ComputedRef<any>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ReactiveTabReturn {
|
|
18
|
+
routeTabTitle: Ref<string> | ComputedRef<string>
|
|
19
|
+
routeTabIcon: Ref<string> | ComputedRef<string>
|
|
20
|
+
routeTabClosable: Ref<boolean> | ComputedRef<boolean>
|
|
21
|
+
routeTabMeta: Ref<any> | ComputedRef<any>
|
|
22
|
+
updateTitle: (title: string) => void
|
|
23
|
+
updateIcon: (icon: string) => void
|
|
24
|
+
updateClosable: (closable: boolean) => void
|
|
25
|
+
updateMeta: (meta: any) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Composable for managing reactive tab properties
|
|
30
|
+
* RouterTab will automatically watch these properties and update the tab accordingly
|
|
31
|
+
*/
|
|
32
|
+
function resolveReactiveProp<T>(
|
|
33
|
+
source: T | Ref<T> | ComputedRef<T> | (() => T) | undefined,
|
|
34
|
+
fallback: T
|
|
35
|
+
): {
|
|
36
|
+
value: Ref<T> | ComputedRef<T>
|
|
37
|
+
update: (value: T) => void
|
|
38
|
+
} {
|
|
39
|
+
if (isRef(source)) {
|
|
40
|
+
const writable = !isReadonly(source)
|
|
41
|
+
return {
|
|
42
|
+
value: source,
|
|
43
|
+
update: writable ? (value: T) => { (source as Ref<T>).value = value } : () => {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof source === 'function') {
|
|
48
|
+
const getter = source as () => T
|
|
49
|
+
return {
|
|
50
|
+
value: computed(getter),
|
|
51
|
+
update: () => {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const state = ref(
|
|
56
|
+
(source === undefined ? fallback : source) as T
|
|
57
|
+
) as Ref<T>
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
value: state,
|
|
61
|
+
update: (value: T) => {
|
|
62
|
+
state.value = value
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useReactiveTab(initialState: ReactiveTabState = {}): ReactiveTabReturn {
|
|
68
|
+
const title = resolveReactiveProp(initialState.title, 'Untitled')
|
|
69
|
+
const icon = resolveReactiveProp(initialState.icon, '')
|
|
70
|
+
const closable = resolveReactiveProp(initialState.closable, true)
|
|
71
|
+
const meta = resolveReactiveProp(initialState.meta, {})
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
routeTabTitle: title.value,
|
|
75
|
+
routeTabIcon: icon.value,
|
|
76
|
+
routeTabClosable: closable.value,
|
|
77
|
+
routeTabMeta: meta.value,
|
|
78
|
+
updateTitle: title.update,
|
|
79
|
+
updateIcon: icon.update,
|
|
80
|
+
updateClosable: closable.update,
|
|
81
|
+
updateMeta: meta.update
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Preset for loading state tabs
|
|
87
|
+
*/
|
|
88
|
+
export function useLoadingTab(loadingState: Ref<boolean>, baseTitle = 'Page') {
|
|
89
|
+
return useReactiveTab({
|
|
90
|
+
title: computed(() => loadingState.value ? 'Loading...' : baseTitle),
|
|
91
|
+
icon: computed(() => loadingState.value ? 'mdi-loading mdi-spin' : 'mdi-page'),
|
|
92
|
+
closable: computed(() => !loadingState.value)
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Preset for notification-aware tabs
|
|
98
|
+
*/
|
|
99
|
+
export function useNotificationTab(
|
|
100
|
+
count: Ref<number>,
|
|
101
|
+
baseTitle = 'Page',
|
|
102
|
+
baseIcon = 'mdi-page'
|
|
103
|
+
) {
|
|
104
|
+
return useReactiveTab({
|
|
105
|
+
title: computed(() => count.value > 0 ? `${baseTitle} (${count.value})` : baseTitle),
|
|
106
|
+
icon: computed(() => count.value > 0 ? 'mdi-bell-badge' : baseIcon)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Preset for status-aware tabs
|
|
112
|
+
*/
|
|
113
|
+
export function useStatusTab(
|
|
114
|
+
status: Ref<'normal' | 'loading' | 'error' | 'success'>,
|
|
115
|
+
baseTitle = 'Page'
|
|
116
|
+
) {
|
|
117
|
+
const statusConfig = {
|
|
118
|
+
normal: { suffix: '', icon: 'mdi-page' },
|
|
119
|
+
loading: { suffix: ' - Loading', icon: 'mdi-loading mdi-spin' },
|
|
120
|
+
error: { suffix: ' - Error', icon: 'mdi-alert' },
|
|
121
|
+
success: { suffix: ' - Success', icon: 'mdi-check-circle' }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return useReactiveTab({
|
|
125
|
+
title: computed(() => baseTitle + statusConfig[status.value].suffix),
|
|
126
|
+
icon: computed(() => statusConfig[status.value].icon),
|
|
127
|
+
closable: computed(() => status.value !== 'loading')
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vue3-router-tab",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"./package.json": "./package.json"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"build": "vite build"
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
@@ -46,7 +47,11 @@
|
|
|
46
47
|
],
|
|
47
48
|
"license": "MIT",
|
|
48
49
|
"homepage": "https://github.com/anilshr25/vue3-router-tab/#readme",
|
|
49
|
-
"description": "
|
|
50
|
+
"description": "Vue 3 Router Tabs component: tabbed navigation for Vue Router with persistence, context menu, drag-and-drop reordering, theming, and reactive page-driven tab titles.",
|
|
51
|
+
"sideEffects": [
|
|
52
|
+
"dist/vue3-router-tab.css",
|
|
53
|
+
"lib/scss/index.scss"
|
|
54
|
+
],
|
|
50
55
|
"repository": {
|
|
51
56
|
"type": "git",
|
|
52
57
|
"url": "https://github.com/anilshr25/vue3-router-tab.git"
|