vue3-router-tab 1.1.8 → 1.2.0
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 +1 -1
- package/lib/components/RouterTab.vue +106 -13
- package/lib/core/createRouterTabs.ts +2 -1
- package/lib/persistence.ts +9 -1
- package/lib/scss/index.scss +117 -128
- package/lib/scss/variables.scss +47 -0
- package/lib/theme.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ The composable also exposes `serialize` / `deserialize` options so you can encry
|
|
|
115
115
|
The plugin initialises a lightweight theme layer on install:
|
|
116
116
|
|
|
117
117
|
- Reads `tab-theme-style` (`'light'`, `'dark'`, or `'system'`; defaults to `'system'`).
|
|
118
|
-
- Reads `tab-theme-primary-color` (defaults to `#
|
|
118
|
+
- Reads `tab-theme-primary-color` (defaults to `#0f172a`).
|
|
119
119
|
- Applies the choice via `data-theme` and `--theme-primary` CSS variables, keeping “system” in sync with OS changes.
|
|
120
120
|
|
|
121
121
|
Override the theme at runtime:
|
|
@@ -12,22 +12,28 @@
|
|
|
12
12
|
v-bind="tabTransitionProps"
|
|
13
13
|
>
|
|
14
14
|
<li
|
|
15
|
-
v-for="tab in tabs"
|
|
15
|
+
v-for="(tab, index) in tabs"
|
|
16
16
|
:key="tab.id"
|
|
17
17
|
:class="buildTabClass(tab)"
|
|
18
|
-
:data-title="
|
|
18
|
+
:data-title="getTabTitle(tab)"
|
|
19
|
+
:draggable="sortable"
|
|
19
20
|
@click="activate(tab)"
|
|
20
21
|
@auxclick.middle.prevent="close(tab)"
|
|
21
22
|
@contextmenu.prevent="showContextMenu(tab, $event)"
|
|
23
|
+
@dragstart="onDragStart(tab, index, $event)"
|
|
24
|
+
@dragover="onDragOver(index, $event)"
|
|
25
|
+
@dragenter="onDragEnter(index)"
|
|
26
|
+
@dragleave="onDragLeave"
|
|
27
|
+
@drop="onDrop(index, $event)"
|
|
28
|
+
@dragend="onDragEnd"
|
|
22
29
|
>
|
|
23
|
-
<span class="router-tab__item-title" :title="
|
|
30
|
+
<span class="router-tab__item-title" :title="getTabTitle(tab)">
|
|
24
31
|
<i v-if="tab.icon" :class="['router-tab__item-icon', tab.icon]" />
|
|
25
|
-
{{
|
|
32
|
+
{{ getTabTitle(tab) }}
|
|
26
33
|
</span>
|
|
27
34
|
<a
|
|
28
35
|
v-if="isClosable(tab)"
|
|
29
36
|
class="router-tab__item-close"
|
|
30
|
-
type="button"
|
|
31
37
|
@click.stop="close(tab)"
|
|
32
38
|
/>
|
|
33
39
|
</li>
|
|
@@ -125,7 +131,6 @@ import { getTransOpt } from '../util/index'
|
|
|
125
131
|
import { routerTabsKey, routerTabsCookie } from '../constants'
|
|
126
132
|
import { useRouterTabsPersistence } from '../persistence'
|
|
127
133
|
|
|
128
|
-
|
|
129
134
|
interface ResolvedMenuItem {
|
|
130
135
|
id: string
|
|
131
136
|
label: string
|
|
@@ -180,9 +185,18 @@ export default defineComponent({
|
|
|
180
185
|
persistence: {
|
|
181
186
|
type: Object as PropType<RouterTabsPersistenceOptions | null>,
|
|
182
187
|
default: null
|
|
188
|
+
},
|
|
189
|
+
sortable: {
|
|
190
|
+
type: Boolean,
|
|
191
|
+
default: true
|
|
192
|
+
},
|
|
193
|
+
titleResolver: {
|
|
194
|
+
type: Function as PropType<(tab: TabRecord) => string>,
|
|
195
|
+
default: null
|
|
183
196
|
}
|
|
184
197
|
},
|
|
185
|
-
|
|
198
|
+
emits: ['tab-sort', 'tab-sorted'],
|
|
199
|
+
setup(props, { emit }) {
|
|
186
200
|
const instance = getCurrentInstance()
|
|
187
201
|
if (!instance) {
|
|
188
202
|
throw new Error('[RouterTab] component must be used within a Vue application context.')
|
|
@@ -199,7 +213,7 @@ export default defineComponent({
|
|
|
199
213
|
maxAlive: props.maxAlive,
|
|
200
214
|
keepLastTab: props.keepLastTab,
|
|
201
215
|
appendPosition: props.append,
|
|
202
|
-
defaultRoute: props.defaultPage
|
|
216
|
+
defaultRoute: props.defaultPage,
|
|
203
217
|
})
|
|
204
218
|
|
|
205
219
|
provide(routerTabsKey, controller)
|
|
@@ -228,6 +242,14 @@ export default defineComponent({
|
|
|
228
242
|
position: { x: 0, y: 0 }
|
|
229
243
|
})
|
|
230
244
|
|
|
245
|
+
// Drag and drop state
|
|
246
|
+
const dragState = reactive({
|
|
247
|
+
dragging: false,
|
|
248
|
+
dragIndex: -1,
|
|
249
|
+
dropIndex: -1,
|
|
250
|
+
dragTab: null as TabRecord | null
|
|
251
|
+
})
|
|
252
|
+
|
|
231
253
|
type MenuConfig = RouterTabsMenuConfig
|
|
232
254
|
type MenuActionContext = RouterTabsMenuContext
|
|
233
255
|
type CustomMenuOption = RouterTabsMenuItem
|
|
@@ -385,7 +407,10 @@ export default defineComponent({
|
|
|
385
407
|
await item.action()
|
|
386
408
|
}
|
|
387
409
|
|
|
388
|
-
function
|
|
410
|
+
function getTabTitle(tab: TabRecord): string {
|
|
411
|
+
if (props.titleResolver) {
|
|
412
|
+
return props.titleResolver(tab)
|
|
413
|
+
}
|
|
389
414
|
if (typeof tab.title === 'string') return tab.title
|
|
390
415
|
if (Array.isArray(tab.title) && tab.title.length) return String(tab.title[0])
|
|
391
416
|
return tab.fullPath
|
|
@@ -411,7 +436,9 @@ export default defineComponent({
|
|
|
411
436
|
'router-tab__item',
|
|
412
437
|
{
|
|
413
438
|
'is-active': controller.activeId.value === tab.id,
|
|
414
|
-
'is-closable': isClosable(tab)
|
|
439
|
+
'is-closable': isClosable(tab),
|
|
440
|
+
'is-dragging': dragState.dragging && dragState.dragTab?.id === tab.id,
|
|
441
|
+
'is-drag-over': dragState.dropIndex === getTabIndex(tab.id)
|
|
415
442
|
},
|
|
416
443
|
tab.tabClass
|
|
417
444
|
]
|
|
@@ -421,6 +448,66 @@ export default defineComponent({
|
|
|
421
448
|
return controller.refreshingKey.value === controller.getRouteKey(route)
|
|
422
449
|
}
|
|
423
450
|
|
|
451
|
+
// Drag and drop handlers
|
|
452
|
+
function onDragStart(tab: TabRecord, index: number, event: DragEvent) {
|
|
453
|
+
if (!props.sortable) return
|
|
454
|
+
|
|
455
|
+
dragState.dragging = true
|
|
456
|
+
dragState.dragIndex = index
|
|
457
|
+
dragState.dragTab = tab
|
|
458
|
+
|
|
459
|
+
if (event.dataTransfer) {
|
|
460
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
461
|
+
event.dataTransfer.setData('text/plain', tab.id)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
emit('tab-sort', { tab, index })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function onDragOver(index: number, event: DragEvent) {
|
|
468
|
+
if (!props.sortable || !dragState.dragging) return
|
|
469
|
+
event.preventDefault()
|
|
470
|
+
if (event.dataTransfer) {
|
|
471
|
+
event.dataTransfer.dropEffect = 'move'
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function onDragEnter(index: number) {
|
|
476
|
+
if (!props.sortable || !dragState.dragging) return
|
|
477
|
+
dragState.dropIndex = index
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function onDragLeave() {
|
|
481
|
+
if (!props.sortable || !dragState.dragging) return
|
|
482
|
+
// Don't reset dropIndex immediately to prevent flicker
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function onDrop(index: number, event: DragEvent) {
|
|
486
|
+
if (!props.sortable || !dragState.dragging) return
|
|
487
|
+
|
|
488
|
+
event.preventDefault()
|
|
489
|
+
|
|
490
|
+
if (dragState.dragIndex !== -1 && dragState.dragIndex !== index) {
|
|
491
|
+
const movedTab = controller.tabs.splice(dragState.dragIndex, 1)[0]
|
|
492
|
+
controller.tabs.splice(index, 0, movedTab)
|
|
493
|
+
|
|
494
|
+
emit('tab-sorted', {
|
|
495
|
+
tab: movedTab,
|
|
496
|
+
fromIndex: dragState.dragIndex,
|
|
497
|
+
toIndex: index
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
onDragEnd()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function onDragEnd() {
|
|
505
|
+
dragState.dragging = false
|
|
506
|
+
dragState.dragIndex = -1
|
|
507
|
+
dragState.dropIndex = -1
|
|
508
|
+
dragState.dragTab = null
|
|
509
|
+
}
|
|
510
|
+
|
|
424
511
|
onMounted(() => {
|
|
425
512
|
document.addEventListener('keydown', hideContextMenu)
|
|
426
513
|
})
|
|
@@ -474,11 +561,17 @@ export default defineComponent({
|
|
|
474
561
|
handleMenuAction,
|
|
475
562
|
showContextMenu,
|
|
476
563
|
hideContextMenu,
|
|
477
|
-
|
|
564
|
+
getTabTitle,
|
|
478
565
|
isClosable,
|
|
479
566
|
isRefreshing,
|
|
480
|
-
hasCustomSlot
|
|
567
|
+
hasCustomSlot,
|
|
568
|
+
onDragStart,
|
|
569
|
+
onDragOver,
|
|
570
|
+
onDragEnter,
|
|
571
|
+
onDragLeave,
|
|
572
|
+
onDrop,
|
|
573
|
+
onDragEnd
|
|
481
574
|
}
|
|
482
575
|
}
|
|
483
576
|
})
|
|
484
|
-
</script>
|
|
577
|
+
</script>
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
CloseTabOptions,
|
|
4
4
|
RouteMatchResult,
|
|
5
5
|
RouterTabsContext,
|
|
6
|
+
RouterTabsMenuPreset,
|
|
6
7
|
RouterTabsOptions,
|
|
7
8
|
RouterTabsSnapshot,
|
|
8
9
|
RouterTabsSnapshotTab,
|
|
@@ -215,7 +216,7 @@ export function createRouterTabs(
|
|
|
215
216
|
return options.defaultRoute
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
async function closeTab(id: string |
|
|
219
|
+
async function closeTab(id: string | null = activeId.value, closeOptions: CloseTabOptions = {}) {
|
|
219
220
|
if (!id) return
|
|
220
221
|
if (!closeOptions.force && options.keepLastTab && tabs.length === 1) {
|
|
221
222
|
throw new Error('[RouterTabs] Unable to close the final tab when keepLastTab is true.')
|
package/lib/persistence.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onMounted, ref, watch } from 'vue'
|
|
1
|
+
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
2
2
|
import type { RouteLocationRaw } from 'vue-router'
|
|
3
3
|
import { useRouterTabs } from './useRouterTabs'
|
|
4
4
|
import type { RouterTabsSnapshot } from './core/types'
|
|
@@ -108,6 +108,14 @@ export function useRouterTabsPersistence(options: RouterTabsPersistenceOptions =
|
|
|
108
108
|
try {
|
|
109
109
|
hydrating.value = true
|
|
110
110
|
await ctrl.hydrate(initialSnapshot)
|
|
111
|
+
if (initialSnapshot.active) {
|
|
112
|
+
await nextTick()
|
|
113
|
+
const activeTab = ctrl.tabs.find(t => t.to === initialSnapshot.active)
|
|
114
|
+
if (activeTab) {
|
|
115
|
+
ctrl.activeId.value = activeTab.id
|
|
116
|
+
ctrl.current.value = activeTab
|
|
117
|
+
}
|
|
118
|
+
}
|
|
111
119
|
} finally {
|
|
112
120
|
hydrating.value = false
|
|
113
121
|
}
|
package/lib/scss/index.scss
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
@use "sass:math";
|
|
2
|
-
@use "sass:
|
|
2
|
+
@use "sass:color";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
$
|
|
6
|
-
$
|
|
7
|
-
$
|
|
8
|
-
$
|
|
4
|
+
$default-primary: #0f172a;
|
|
5
|
+
$default-border: #e2e8f0;
|
|
6
|
+
$default-text: #1e293b;
|
|
7
|
+
$default-bg: #f8fafc;
|
|
8
|
+
$default-dark-bg: #0f172a;
|
|
9
|
+
$default-dark-text: #e2e8f0;
|
|
10
|
+
$default-dark-border: rgba(148, 163, 184, 0.35);
|
|
9
11
|
|
|
10
12
|
$font-size: 14px;
|
|
11
13
|
$tab-trans: all 0.3s ease-in-out;
|
|
@@ -14,26 +16,48 @@ $tab-padding: 20px;
|
|
|
14
16
|
$close-icon-margin: 4px;
|
|
15
17
|
$close-icon-size: 13px;
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
:root {
|
|
20
|
+
--theme-primary: #{$default-primary};
|
|
21
|
+
--router-tab-primary: var(--theme-primary);
|
|
22
|
+
--router-tab-background: #{$default-bg};
|
|
23
|
+
--router-tab-foreground: #{$default-text};
|
|
24
|
+
--router-tab-border: #{$default-border};
|
|
25
|
+
--router-tab-header-bg: #{$default-bg};
|
|
26
|
+
color-scheme: light;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
:root[data-theme="light"] {
|
|
30
|
+
color-scheme: light;
|
|
31
|
+
--router-tab-background: #{$default-bg};
|
|
32
|
+
--router-tab-foreground: #{$default-text};
|
|
33
|
+
--router-tab-border: #{$default-border};
|
|
34
|
+
--router-tab-header-bg: #{$default-bg};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:root[data-theme="dark"] {
|
|
38
|
+
color-scheme: dark;
|
|
39
|
+
--router-tab-background: #{$default-dark-bg};
|
|
40
|
+
--router-tab-foreground: #{$default-dark-text};
|
|
41
|
+
--router-tab-border: #{$default-dark-border};
|
|
42
|
+
--router-tab-header-bg: #{$default-dark-bg};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@media (prefers-color-scheme: dark) {
|
|
46
|
+
:root:not([data-theme]) {
|
|
47
|
+
color-scheme: dark;
|
|
48
|
+
--router-tab-background: #{$default-dark-bg};
|
|
49
|
+
--router-tab-foreground: #{$default-dark-text};
|
|
50
|
+
--router-tab-border: #{$default-dark-border};
|
|
51
|
+
--router-tab-header-bg: #{$default-dark-bg};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
31
54
|
|
|
55
|
+
.router-tab {
|
|
32
56
|
display: flex;
|
|
33
57
|
flex-direction: column;
|
|
34
58
|
min-height: 300px;
|
|
35
|
-
background-color:
|
|
36
|
-
color:
|
|
59
|
+
background-color: var(--router-tab-background);
|
|
60
|
+
color: var(--router-tab-foreground);
|
|
37
61
|
|
|
38
62
|
&__header {
|
|
39
63
|
position: relative;
|
|
@@ -42,8 +66,8 @@ $close-icon-size: 13px;
|
|
|
42
66
|
flex: none;
|
|
43
67
|
box-sizing: border-box;
|
|
44
68
|
height: $hd-height;
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
background-color: var(--router-tab-header-bg);
|
|
70
|
+
border-bottom: 1px solid var(--router-tab-border); // FIXED: Added border
|
|
47
71
|
transition: all 0.2s ease-in-out;
|
|
48
72
|
}
|
|
49
73
|
|
|
@@ -117,38 +141,40 @@ $close-icon-size: 13px;
|
|
|
117
141
|
padding: 0 $tab-padding;
|
|
118
142
|
color: inherit;
|
|
119
143
|
font-size: $font-size;
|
|
120
|
-
border: 1px solid $border;
|
|
121
|
-
border-left: none;
|
|
122
144
|
transform-origin: left bottom;
|
|
123
145
|
cursor: pointer;
|
|
124
146
|
transition: $tab-trans;
|
|
125
147
|
user-select: none;
|
|
126
148
|
background-color: transparent;
|
|
149
|
+
|
|
150
|
+
// FIXED: Added proper borders
|
|
151
|
+
border-right: 1px solid var(--router-tab-border);
|
|
152
|
+
border-top: 1px solid transparent;
|
|
153
|
+
border-bottom: 2px solid transparent;
|
|
127
154
|
|
|
128
155
|
&:first-child {
|
|
129
|
-
border-left: 1px solid
|
|
156
|
+
border-left: 1px solid var(--router-tab-border);
|
|
130
157
|
}
|
|
131
158
|
|
|
132
159
|
&.is-contextmenu {
|
|
133
|
-
color:
|
|
160
|
+
color: var(--router-tab-primary);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// NEW: Drag states
|
|
164
|
+
&.is-dragging {
|
|
165
|
+
opacity: 0.5;
|
|
166
|
+
cursor: move;
|
|
134
167
|
}
|
|
135
168
|
|
|
136
169
|
&.is-drag-over {
|
|
137
|
-
background:
|
|
138
|
-
|
|
170
|
+
background: color-mix(in srgb, var(--router-tab-primary) 10%, transparent 90%);
|
|
171
|
+
border-left: 2px solid var(--router-tab-primary);
|
|
172
|
+
transition: background 0.15s ease, border-left 0.15s ease;
|
|
139
173
|
}
|
|
140
174
|
|
|
141
175
|
&-title {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
align-items: center;
|
|
145
|
-
gap: var(--router-tab-title-gap, 0);
|
|
146
|
-
min-width: var(--router-tab-title-min-width, 30px);
|
|
147
|
-
max-width: var(--router-tab-title-max-width, 120px);
|
|
148
|
-
color: var(--router-tab-title-color, inherit);
|
|
149
|
-
font-weight: var(--router-tab-title-font-weight, inherit);
|
|
150
|
-
letter-spacing: var(--router-tab-title-letter-spacing, normal);
|
|
151
|
-
text-transform: var(--router-tab-title-transform, none);
|
|
176
|
+
min-width: 30px;
|
|
177
|
+
max-width: 120px;
|
|
152
178
|
overflow: hidden;
|
|
153
179
|
white-space: nowrap;
|
|
154
180
|
text-overflow: ellipsis;
|
|
@@ -161,7 +187,8 @@ $close-icon-size: 13px;
|
|
|
161
187
|
|
|
162
188
|
&:hover,
|
|
163
189
|
&.is-active {
|
|
164
|
-
color:
|
|
190
|
+
color: var(--router-tab-primary);
|
|
191
|
+
background-color: color-mix(in srgb, var(--router-tab-primary) 5%, transparent 95%);
|
|
165
192
|
|
|
166
193
|
&.is-closable {
|
|
167
194
|
padding: 0 ($tab-padding - math.div($close-icon-size + $close-icon-margin, 2));
|
|
@@ -173,83 +200,19 @@ $close-icon-size: 13px;
|
|
|
173
200
|
|
|
174
201
|
&::before,
|
|
175
202
|
&::after {
|
|
176
|
-
background-color:
|
|
203
|
+
background-color: currentColor;
|
|
177
204
|
}
|
|
178
205
|
}
|
|
179
206
|
}
|
|
180
207
|
|
|
181
|
-
|
|
182
|
-
.router-tab__item-title {
|
|
183
|
-
max-width: var(--router-tab-title-hover-max-width, 220px);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
208
|
+
// FIXED: Enhanced active state
|
|
187
209
|
&.is-active {
|
|
188
|
-
border-bottom
|
|
189
|
-
background:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
content: '';
|
|
195
|
-
position: absolute;
|
|
196
|
-
left: 16px;
|
|
197
|
-
right: 16px;
|
|
198
|
-
bottom: 6px;
|
|
199
|
-
height: 2px;
|
|
200
|
-
border-radius: 999px;
|
|
201
|
-
background: $primary;
|
|
202
|
-
opacity: 0.6;
|
|
203
|
-
pointer-events: none;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
&[data-title] {
|
|
208
|
-
&::after {
|
|
209
|
-
content: attr(data-title);
|
|
210
|
-
position: absolute;
|
|
211
|
-
left: 50%;
|
|
212
|
-
bottom: calc(100% + 8px);
|
|
213
|
-
display: inline-block;
|
|
214
|
-
max-width: 320px;
|
|
215
|
-
padding: 4px 10px;
|
|
216
|
-
border-radius: 6px;
|
|
217
|
-
background: $tooltip-bg;
|
|
218
|
-
color: $tooltip-fg;
|
|
219
|
-
font-size: 12px;
|
|
220
|
-
line-height: 1.3;
|
|
221
|
-
text-align: center;
|
|
222
|
-
white-space: normal;
|
|
223
|
-
word-break: break-word;
|
|
224
|
-
transform: translate(-50%, 4px);
|
|
225
|
-
box-shadow: $tooltip-shadow;
|
|
226
|
-
opacity: 0;
|
|
227
|
-
pointer-events: none;
|
|
228
|
-
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
&::before {
|
|
232
|
-
content: '';
|
|
233
|
-
position: absolute;
|
|
234
|
-
left: 50%;
|
|
235
|
-
bottom: calc(100% + 4px);
|
|
236
|
-
width: 8px;
|
|
237
|
-
height: 8px;
|
|
238
|
-
background: $tooltip-bg;
|
|
239
|
-
transform: translate(-50%, 6px) rotate(45deg);
|
|
240
|
-
opacity: 0;
|
|
241
|
-
pointer-events: none;
|
|
242
|
-
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
&:hover::after {
|
|
246
|
-
opacity: 1;
|
|
247
|
-
transform: translate(-50%, 0);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
&:hover::before {
|
|
251
|
-
opacity: 1;
|
|
252
|
-
transform: translate(-50%, 0) rotate(45deg);
|
|
210
|
+
border-bottom: 2px solid var(--router-tab-primary);
|
|
211
|
+
background-color: var(--router-tab-background);
|
|
212
|
+
font-weight: 500;
|
|
213
|
+
|
|
214
|
+
&:not(:first-child) {
|
|
215
|
+
border-left-color: var(--router-tab-border);
|
|
253
216
|
}
|
|
254
217
|
}
|
|
255
218
|
|
|
@@ -279,7 +242,7 @@ $close-icon-size: 13px;
|
|
|
279
242
|
margin-left: math.div(-$inner, 2);
|
|
280
243
|
background-color: currentColor;
|
|
281
244
|
transition: background-color 0.2s ease-in-out;
|
|
282
|
-
content:
|
|
245
|
+
content: "";
|
|
283
246
|
}
|
|
284
247
|
|
|
285
248
|
&::before {
|
|
@@ -291,7 +254,7 @@ $close-icon-size: 13px;
|
|
|
291
254
|
}
|
|
292
255
|
|
|
293
256
|
&:hover {
|
|
294
|
-
background-color: color-mix(in srgb,
|
|
257
|
+
background-color: color-mix(in srgb, var(--router-tab-primary) 40%, #ffffff 60%);
|
|
295
258
|
|
|
296
259
|
&::before,
|
|
297
260
|
&::after {
|
|
@@ -301,14 +264,15 @@ $close-icon-size: 13px;
|
|
|
301
264
|
}
|
|
302
265
|
}
|
|
303
266
|
|
|
267
|
+
// FIXED: Improved context menu z-index
|
|
304
268
|
&__contextmenu {
|
|
305
269
|
position: fixed;
|
|
306
|
-
z-index: 1000
|
|
270
|
+
z-index: 9999; // Increased from 1000
|
|
307
271
|
min-width: 140px;
|
|
308
272
|
padding: 8px 0;
|
|
309
273
|
font-size: $font-size;
|
|
310
|
-
background:
|
|
311
|
-
border: 1px solid
|
|
274
|
+
background: var(--router-tab-background);
|
|
275
|
+
border: 1px solid var(--router-tab-border);
|
|
312
276
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.15);
|
|
313
277
|
border-radius: 8px;
|
|
314
278
|
|
|
@@ -324,29 +288,54 @@ $close-icon-size: 13px;
|
|
|
324
288
|
color: inherit;
|
|
325
289
|
cursor: pointer;
|
|
326
290
|
font: inherit;
|
|
291
|
+
text-decoration: none;
|
|
327
292
|
transition: background-color 0.2s ease-in-out;
|
|
328
293
|
|
|
329
|
-
&[aria-disabled=
|
|
294
|
+
&[aria-disabled="true"] {
|
|
330
295
|
color: rgba(148, 163, 184, 0.6);
|
|
331
296
|
pointer-events: none;
|
|
297
|
+
cursor: not-allowed;
|
|
332
298
|
}
|
|
333
299
|
|
|
334
|
-
&:hover,
|
|
335
|
-
&:focus-visible {
|
|
336
|
-
background: color-mix(in srgb,
|
|
337
|
-
color:
|
|
300
|
+
&:not([aria-disabled="true"]):hover,
|
|
301
|
+
&:not([aria-disabled="true"]):focus-visible {
|
|
302
|
+
background: color-mix(in srgb, var(--router-tab-primary) 15%, transparent 85%);
|
|
303
|
+
color: var(--router-tab-primary);
|
|
304
|
+
outline: none;
|
|
338
305
|
}
|
|
339
306
|
}
|
|
340
307
|
}
|
|
308
|
+
|
|
309
|
+
&__container {
|
|
310
|
+
position: relative;
|
|
311
|
+
flex: 1 1 auto;
|
|
312
|
+
background-color: var(--router-tab-background);
|
|
313
|
+
border: 1px solid var(--router-tab-border);
|
|
314
|
+
border-top: none; // FIXED: Avoid double border with header
|
|
315
|
+
}
|
|
341
316
|
}
|
|
342
317
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
318
|
+
// FIXED: Proper adjacent tab borders
|
|
319
|
+
.router-tab__item {
|
|
320
|
+
+ .router-tab__item {
|
|
321
|
+
border-left-color: var(--router-tab-border);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
&.is-active + .router-tab__item {
|
|
325
|
+
border-left-color: var(--router-tab-border);
|
|
326
|
+
}
|
|
348
327
|
}
|
|
349
328
|
|
|
350
|
-
|
|
351
|
-
|
|
329
|
+
// NEW: Smooth drag animations
|
|
330
|
+
@keyframes router-tab-drag-hint {
|
|
331
|
+
0%, 100% {
|
|
332
|
+
transform: translateY(0);
|
|
333
|
+
}
|
|
334
|
+
50% {
|
|
335
|
+
transform: translateY(-2px);
|
|
336
|
+
}
|
|
352
337
|
}
|
|
338
|
+
|
|
339
|
+
.router-tab__item.is-dragging {
|
|
340
|
+
animation: router-tab-drag-hint 0.3s ease-in-out;
|
|
341
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// variables.scss
|
|
2
|
+
|
|
3
|
+
// Tab colors (light + dark theme overrides in :root)
|
|
4
|
+
$router-tab-bg-light: #ffffff !default;
|
|
5
|
+
$router-tab-bg-dark: #1e293b !default;
|
|
6
|
+
|
|
7
|
+
$router-tab-text-light: #0f172a !default;
|
|
8
|
+
$router-tab-text-dark: #f1f5f9 !default;
|
|
9
|
+
|
|
10
|
+
$router-tab-border-light: #e2e8f0 !default;
|
|
11
|
+
$router-tab-border-dark: #334155 !default;
|
|
12
|
+
|
|
13
|
+
$router-tab-primary: #3b82f6 !default; // default primary color
|
|
14
|
+
|
|
15
|
+
// Sizes
|
|
16
|
+
$router-tab-header-height: 42px !default;
|
|
17
|
+
$router-tab-padding: 12px !default;
|
|
18
|
+
$router-tab-font-size: 14px !default;
|
|
19
|
+
|
|
20
|
+
// Title
|
|
21
|
+
$router-tab-title-min-width: 60px !default;
|
|
22
|
+
$router-tab-title-max-width: 180px !default;
|
|
23
|
+
|
|
24
|
+
// Icons
|
|
25
|
+
$router-tab-icon-size: 14px !default;
|
|
26
|
+
$router-tab-icon-margin: 6px !default;
|
|
27
|
+
|
|
28
|
+
// Close icon
|
|
29
|
+
$router-tab-close-icon-size: 12px !default;
|
|
30
|
+
$router-tab-close-icon-margin: 4px !default;
|
|
31
|
+
|
|
32
|
+
// Context menu
|
|
33
|
+
$router-tab-contextmenu-min-width: 160px !default;
|
|
34
|
+
$router-tab-contextmenu-padding: 6px 0 !default;
|
|
35
|
+
$router-tab-contextmenu-item-height: 32px !default;
|
|
36
|
+
$router-tab-contextmenu-item-padding: 0 12px !default;
|
|
37
|
+
$router-tab-contextmenu-border-radius: 8px !default;
|
|
38
|
+
$router-tab-contextmenu-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !default;
|
|
39
|
+
|
|
40
|
+
// Dragging
|
|
41
|
+
$router-tab-drag-opacity: 0.6 !default;
|
|
42
|
+
$router-tab-drag-cursor: grabbing !default;
|
|
43
|
+
$router-tab-drag-over-bg: rgba(0, 0, 0, 0.05) !default;
|
|
44
|
+
|
|
45
|
+
// Transitions
|
|
46
|
+
$router-tab-transition-fast: all 0.2s ease !default;
|
|
47
|
+
$router-tab-transition: all 0.3s ease !default;
|
package/lib/theme.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const STYLE_KEY = 'tab-theme-style'
|
|
2
2
|
const PRIMARY_KEY = 'tab-theme-primary-color'
|
|
3
3
|
const DEFAULT_STYLE: 'light' | 'dark' | 'system' = 'system'
|
|
4
|
-
const DEFAULT_PRIMARY = '#
|
|
4
|
+
const DEFAULT_PRIMARY = '#0f172a'
|
|
5
5
|
const MEDIA_QUERY = '(prefers-color-scheme: dark)'
|
|
6
6
|
|
|
7
7
|
let mediaListener: ((event: MediaQueryListEvent) => void) | null = null
|