vue3-router-tab 1.2.6 → 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/dist/vue3-router-tab.css +1 -1
- package/dist/vue3-router-tab.js +694 -522
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +189 -8
- package/lib/core/createRouterTabs.ts +18 -0
- package/lib/index.ts +40 -9
- package/lib/persistence.ts +3 -1
- package/lib/scss/index.scss +62 -10
- package/lib/scss/variables.scss +14 -2
- package/lib/theme.ts +39 -4
- package/lib/useReactiveTab.ts +49 -49
- package/package.json +1 -1
package/lib/scss/index.scss
CHANGED
|
@@ -23,7 +23,23 @@
|
|
|
23
23
|
--router-tab-active-background: #{$router-tab-primary};
|
|
24
24
|
--router-tab-active-text: #ffffff;
|
|
25
25
|
--router-tab-active-border: #{$router-tab-primary};
|
|
26
|
-
--router-tab-icon-color: #
|
|
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};
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
// =============================================================================
|
|
@@ -99,6 +115,7 @@
|
|
|
99
115
|
cursor: pointer;
|
|
100
116
|
user-select: none;
|
|
101
117
|
transition: var(--router-tab-transition);
|
|
118
|
+
will-change: background-color, color;
|
|
102
119
|
|
|
103
120
|
&:first-child {
|
|
104
121
|
border-left: 1px solid var(--router-tab-border);
|
|
@@ -165,7 +182,7 @@
|
|
|
165
182
|
overflow: hidden;
|
|
166
183
|
border: none;
|
|
167
184
|
border-radius: 50%;
|
|
168
|
-
background: var(--router-tab-button-
|
|
185
|
+
background: var(--router-tab-button-background);
|
|
169
186
|
color: var(--router-tab-button-color);
|
|
170
187
|
cursor: pointer;
|
|
171
188
|
transition: var(--router-tab-transition);
|
|
@@ -188,7 +205,7 @@
|
|
|
188
205
|
&::after { transform: translateY(-50%) rotate(45deg); }
|
|
189
206
|
|
|
190
207
|
&:hover {
|
|
191
|
-
background: var(--router-tab-active-button-
|
|
208
|
+
background: var(--router-tab-active-button-background);
|
|
192
209
|
color: var(--router-tab-active-button-color);
|
|
193
210
|
}
|
|
194
211
|
}
|
|
@@ -196,15 +213,21 @@
|
|
|
196
213
|
// Show close button on hover/active
|
|
197
214
|
&:hover,
|
|
198
215
|
&.is-active {
|
|
199
|
-
&.is-closable {
|
|
200
|
-
padding-right: calc(var(--router-tab-padding) - #{($router-tab-close-icon-size + $router-tab-close-icon-margin) * 0.5});
|
|
201
|
-
}
|
|
202
216
|
|
|
203
217
|
.router-tab__item-close {
|
|
204
218
|
width: $router-tab-close-icon-size;
|
|
205
219
|
margin-left: $router-tab-close-icon-margin;
|
|
206
220
|
}
|
|
207
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
|
+
}
|
|
208
231
|
}
|
|
209
232
|
|
|
210
233
|
&__container {
|
|
@@ -217,7 +240,29 @@
|
|
|
217
240
|
.router-tab__container {
|
|
218
241
|
padding: 10px;
|
|
219
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%;
|
|
220
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);
|
|
264
|
+
}
|
|
265
|
+
|
|
221
266
|
// Context Menu using button colors
|
|
222
267
|
.router-tab__contextmenu {
|
|
223
268
|
position: fixed;
|
|
@@ -237,13 +282,14 @@
|
|
|
237
282
|
line-height: $router-tab-contextmenu-item-height;
|
|
238
283
|
text-align: left;
|
|
239
284
|
text-decoration: none;
|
|
240
|
-
background: var(--router-tab-button-
|
|
285
|
+
background: var(--router-tab-button-background);
|
|
241
286
|
border: none;
|
|
242
287
|
color: var(--router-tab-button-color);
|
|
243
288
|
cursor: pointer;
|
|
244
289
|
font: inherit;
|
|
245
290
|
border-radius: calc(#{$router-tab-contextmenu-border-radius} - 4px);
|
|
246
291
|
transition: all 0.2s ease-in-out;
|
|
292
|
+
outline: none;
|
|
247
293
|
|
|
248
294
|
&[aria-disabled="true"] {
|
|
249
295
|
color: color-mix(in srgb, var(--router-tab-text) 40%);
|
|
@@ -251,10 +297,16 @@
|
|
|
251
297
|
cursor: not-allowed;
|
|
252
298
|
}
|
|
253
299
|
|
|
254
|
-
&:
|
|
300
|
+
&:focus,
|
|
255
301
|
&:focus-visible {
|
|
256
|
-
|
|
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);
|
|
257
309
|
color: var(--router-tab-active-button-color);
|
|
258
310
|
}
|
|
259
311
|
}
|
|
260
|
-
}
|
|
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
|
@@ -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)
|
|
@@ -122,17 +138,36 @@ export function initRouterTabsTheme(options: RouterTabsThemeOptions = {}) {
|
|
|
122
138
|
|
|
123
139
|
const {
|
|
124
140
|
styleKey = STYLE_KEY,
|
|
141
|
+
primaryKey = PRIMARY_KEY,
|
|
125
142
|
defaultStyle = DEFAULT_STYLE,
|
|
143
|
+
defaultPrimary
|
|
126
144
|
} = options
|
|
127
145
|
|
|
128
146
|
const storedStyle = (window.localStorage.getItem(styleKey) as 'light' | 'dark' | 'system' | null) ?? defaultStyle
|
|
129
147
|
|
|
130
148
|
applyStyle(storedStyle)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
})
|
|
134
169
|
} else {
|
|
135
|
-
applyPrimary(
|
|
170
|
+
applyPrimary(basePalette)
|
|
136
171
|
}
|
|
137
172
|
}
|
|
138
173
|
|
package/lib/useReactiveTab.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ref,
|
|
3
|
+
computed,
|
|
4
|
+
isRef,
|
|
5
|
+
isReadonly,
|
|
6
|
+
type Ref,
|
|
7
|
+
type ComputedRef
|
|
8
|
+
} from 'vue'
|
|
2
9
|
|
|
3
10
|
export interface ReactiveTabState {
|
|
4
11
|
title?: string | Ref<string> | ComputedRef<string>
|
|
@@ -22,64 +29,56 @@ export interface ReactiveTabReturn {
|
|
|
22
29
|
* Composable for managing reactive tab properties
|
|
23
30
|
* RouterTab will automatically watch these properties and update the tab accordingly
|
|
24
31
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const routeTabIcon = typeof initialState.icon === 'function'
|
|
38
|
-
? computed(initialState.icon as () => string)
|
|
39
|
-
: tabIcon
|
|
40
|
-
|
|
41
|
-
const routeTabClosable = typeof initialState.closable === 'function'
|
|
42
|
-
? computed(initialState.closable as () => boolean)
|
|
43
|
-
: tabClosable
|
|
44
|
-
|
|
45
|
-
const routeTabMeta = typeof initialState.meta === 'function'
|
|
46
|
-
? computed(initialState.meta as () => any)
|
|
47
|
-
: tabMeta
|
|
48
|
-
|
|
49
|
-
// Update functions
|
|
50
|
-
const updateTitle = (title: string) => {
|
|
51
|
-
if ('value' in tabTitle) {
|
|
52
|
-
tabTitle.value = title
|
|
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 } : () => {}
|
|
53
44
|
}
|
|
54
45
|
}
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
if (typeof source === 'function') {
|
|
48
|
+
const getter = source as () => T
|
|
49
|
+
return {
|
|
50
|
+
value: computed(getter),
|
|
51
|
+
update: () => {}
|
|
59
52
|
}
|
|
60
53
|
}
|
|
61
54
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
}
|
|
55
|
+
const state = ref(
|
|
56
|
+
(source === undefined ? fallback : source) as T
|
|
57
|
+
) as Ref<T>
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
return {
|
|
60
|
+
value: state,
|
|
61
|
+
update: (value: T) => {
|
|
62
|
+
state.value = value
|
|
71
63
|
}
|
|
72
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, {})
|
|
73
72
|
|
|
74
73
|
return {
|
|
75
|
-
routeTabTitle,
|
|
76
|
-
routeTabIcon,
|
|
77
|
-
routeTabClosable,
|
|
78
|
-
routeTabMeta,
|
|
79
|
-
updateTitle,
|
|
80
|
-
updateIcon,
|
|
81
|
-
updateClosable,
|
|
82
|
-
updateMeta
|
|
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
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
|
|
@@ -127,4 +126,5 @@ export function useStatusTab(
|
|
|
127
126
|
icon: computed(() => statusConfig[status.value].icon),
|
|
128
127
|
closable: computed(() => status.value !== 'loading')
|
|
129
128
|
})
|
|
130
|
-
}
|
|
129
|
+
}
|
|
130
|
+
|