vue3-router-tab 1.1.7 → 1.1.9

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.
@@ -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="tabTitle(tab)"
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="tabTitle(tab)">
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
- {{ tabTitle(tab) }}
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
- setup(props) {
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 tabTitle(tab: TabRecord) {
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
- tabTitle,
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>
@@ -1,64 +1,42 @@
1
1
  @use "sass:math";
2
- @use "sass:string";
3
-
4
- // Fallback palette (overridden by Vuetify CSS vars)
5
- $primary-fallback: #635bff;
6
- $light-bg: #ffffff;
7
- $light-text: #1e293b;
8
- $light-border: rgba(15, 23, 42, 0.08);
9
-
10
- // Dark mode fallbacks
11
- $dark-bg: #1e1e1e;
12
- $dark-text: #e2e8f0;
13
- $dark-border: rgba(226, 232, 240, 0.12);
14
-
15
- $font-size: 14px;
16
- $tab-trans: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
17
- $hd-height: 40px;
18
- $tab-padding: 20px;
19
- $close-icon-margin: 4px;
20
- $close-icon-size: 13px;
21
-
22
- /// Utility to fetch a CSS variable with a graceful fallback.
23
- @function css-var($name, $fallback) {
24
- @return string.unquote("var(#{$name}, #{$fallback})");
2
+ @use "sass:color";
3
+ @use "variables" as *;
4
+
5
+ // Theme Setup
6
+ :root[data-theme="light"] {
7
+ color-scheme: light;
8
+ --router-tab-background: #{$router-tab-bg-light};
9
+ --router-tab-foreground: #{$router-tab-text-light};
10
+ --router-tab-border: #{$router-tab-border-light};
11
+ --router-tab-header-bg: #{$router-tab-bg-light};
25
12
  }
26
13
 
27
- /// Mixin for reduced motion preferences
28
- @mixin reduced-motion {
29
- @media (prefers-reduced-motion: reduce) {
30
- @content;
31
- }
14
+ :root[data-theme="dark"] {
15
+ color-scheme: dark;
16
+ --router-tab-active-border: #ffff;
17
+ --router-tab-background: #{$router-tab-bg-dark};
18
+ --router-tab-foreground: #{$router-tab-text-dark};
19
+ --router-tab-border: #{$router-tab-border-dark};
20
+ --router-tab-header-bg: #{$router-tab-bg-dark};
32
21
  }
33
22
 
34
- /// Mixin for dark theme styles - works with data-theme="dark"
35
- @mixin dark-theme {
36
- :root[data-theme="dark"] & {
37
- @content;
23
+ @media (prefers-color-scheme: dark) {
24
+ :root:not([data-theme]) {
25
+ color-scheme: dark;
26
+ --router-tab-background: #{$router-tab-bg-dark};
27
+ --router-tab-foreground: #{$router-tab-text-dark};
28
+ --router-tab-border: #{$router-tab-border-dark};
29
+ --router-tab-header-bg: #{$router-tab-bg-dark};
38
30
  }
39
31
  }
40
32
 
33
+ // Main Component Styles
41
34
  .router-tab {
42
- // Use Vuetify theme colors with fallbacks
43
- $bg: css-var(--router-tab-background, css-var(--v-theme-surface, css-var(--theme-background, $light-bg)));
44
- $fg: css-var(--router-tab-foreground, css-var(--v-theme-on-surface, css-var(--theme-foreground, $light-text)));
45
- $border: css-var(--router-tab-border, css-var(--v-border-color, css-var(--theme-border, $light-border)));
46
- $primary: css-var(--router-tab-primary, css-var(--v-theme-primary, css-var(--theme-primary, $primary-fallback)));
47
- $header-bg: css-var(--router-tab-header-bg, $bg);
48
- $tooltip-bg: css-var(--router-tab-tooltip-background, rgba(var(--v-theme-on-surface), 0.9));
49
- $tooltip-fg: css-var(--router-tab-tooltip-foreground, css-var(--v-theme-surface, #ffffff));
50
- $tooltip-shadow: css-var(--router-tab-tooltip-shadow, 0 8px 24px rgba(0, 0, 0, 0.18));
51
-
52
35
  display: flex;
53
36
  flex-direction: column;
54
37
  min-height: 300px;
55
- background-color: transparent;
56
- color: inherit;
57
-
58
- // Dark theme default values
59
- @include dark-theme {
60
- --router-tab-tooltip-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
61
- }
38
+ background-color: var(--router-tab-background);
39
+ color: var(--router-tab-foreground);
62
40
 
63
41
  &__header {
64
42
  position: relative;
@@ -66,21 +44,22 @@ $close-icon-size: 13px;
66
44
  display: flex;
67
45
  flex: none;
68
46
  box-sizing: border-box;
69
- height: $hd-height;
70
- border-bottom: 1px solid $border;
71
- background-color: $header-bg;
72
- transition: border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
73
- background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
74
-
75
- @include reduced-motion {
76
- transition: none;
77
- }
47
+ height: var(--router-tab-header-height, $router-tab-header-height);
48
+ background-color: var(--router-tab-header-bg);
49
+ transition: $router-tab-transition-fast;
50
+ border-bottom: 1px solid var(--router-tab-border);
51
+ }
52
+
53
+ &__slot-start,
54
+ &__slot-end {
55
+ display: flex;
56
+ align-items: center;
78
57
  }
79
58
 
80
59
  &__scroll {
81
60
  position: relative;
82
61
  flex: 1 1 0px;
83
- height: $hd-height;
62
+ height: var(--router-tab-header-height, $router-tab-header-height);
84
63
  overflow: hidden;
85
64
 
86
65
  &-container {
@@ -91,7 +70,6 @@ $close-icon-size: 13px;
91
70
  &.is-mobile {
92
71
  overflow-x: auto;
93
72
  overflow-y: hidden;
94
- -webkit-overflow-scrolling: touch;
95
73
  }
96
74
  }
97
75
  }
@@ -104,14 +82,10 @@ $close-icon-size: 13px;
104
82
  bottom: 0;
105
83
  left: 0;
106
84
  height: $h;
107
- background-color: rgba(var(--v-theme-on-surface, 0, 0, 0), 0.1);
85
+ background-color: rgba(0, 0, 0, 0.1);
108
86
  border-radius: $h;
109
87
  opacity: 0;
110
- transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
111
-
112
- @include reduced-motion {
113
- transition: none;
114
- }
88
+ transition: opacity 0.3s ease-in-out;
115
89
 
116
90
  .router-tab__scroll:hover &,
117
91
  &.is-dragging {
@@ -123,17 +97,13 @@ $close-icon-size: 13px;
123
97
  top: 0;
124
98
  left: 0;
125
99
  height: 100%;
126
- background-color: rgba(var(--v-theme-on-surface, 0, 0, 0), 0.15);
100
+ background-color: rgba(0, 0, 0, 0.1);
127
101
  border-radius: $h;
128
- transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
129
-
130
- @include reduced-motion {
131
- transition: none;
132
- }
102
+ transition: background-color 0.3s ease-in-out;
133
103
 
134
104
  &:hover,
135
105
  .router-tab__scrollbar.is-dragging & {
136
- background-color: rgba(var(--v-theme-on-surface, 0, 0, 0), 0.25);
106
+ background-color: rgba(0, 0, 0, 0.2);
137
107
  }
138
108
  }
139
109
  }
@@ -153,180 +123,90 @@ $close-icon-size: 13px;
153
123
  display: flex;
154
124
  flex: none;
155
125
  align-items: center;
156
- padding: 0 $tab-padding;
126
+ padding: 0 var(--router-tab-padding, $router-tab-padding);
157
127
  color: inherit;
158
- font-size: $font-size;
159
- border: 1px solid $border;
160
- border-left: none;
128
+ font-size: var(--router-tab-font-size, $router-tab-font-size);
161
129
  transform-origin: left bottom;
162
130
  cursor: pointer;
163
- transition: $tab-trans;
131
+ transition: var(--router-tab-transition, $router-tab-transition);
164
132
  user-select: none;
165
133
  background-color: transparent;
166
-
167
- @include reduced-motion {
168
- transition: none;
169
- }
134
+ border-right: 1px solid var(--router-tab-border);
170
135
 
171
136
  &:first-child {
172
- border-left: 1px solid $border;
137
+ border-left: 1px solid var(--router-tab-border);
173
138
  }
174
139
 
175
- // Keyboard focus styles using Vuetify primary color
176
- &:focus-visible {
177
- outline: 2px solid $primary;
178
- outline-offset: -2px;
179
- z-index: 1;
140
+ &.is-contextmenu {
141
+ color: var(--router-tab-primary);
180
142
  }
181
143
 
182
- &.is-contextmenu {
183
- color: $primary;
144
+ // Drag and drop states
145
+ &.is-dragging {
146
+ opacity: $router-tab-drag-opacity;
147
+ cursor: $router-tab-drag-cursor;
184
148
  }
185
149
 
186
150
  &.is-drag-over {
187
- background: rgba(var(--v-theme-on-surface, 0, 0, 0), 0.05);
188
- transition: background 0.15s cubic-bezier(0.4, 0, 0.2, 1);
151
+ background: $router-tab-drag-over-bg;
152
+ transition: background 0.15s ease;
153
+
154
+ &::before {
155
+ content: "";
156
+ position: absolute;
157
+ left: -2px;
158
+ top: 0;
159
+ bottom: 0;
160
+ width: 2px;
161
+ background: var(--router-tab-primary);
162
+ }
163
+ }
164
+
165
+ &[draggable="true"] {
166
+ cursor: $router-tab-drag-cursor;
189
167
  }
190
168
 
191
169
  &-title {
192
- position: relative;
193
- display: inline-flex;
194
- align-items: center;
195
- gap: var(--router-tab-title-gap, 0);
196
- min-width: var(--router-tab-title-min-width, 30px);
197
- max-width: var(--router-tab-title-max-width, 120px);
198
- color: var(--router-tab-title-color, inherit);
199
- font-weight: var(--router-tab-title-font-weight, inherit);
200
- letter-spacing: var(--router-tab-title-letter-spacing, normal);
201
- text-transform: var(--router-tab-title-transform, none);
170
+ min-width: $router-tab-title-min-width;
171
+ max-width: $router-tab-title-max-width;
202
172
  overflow: hidden;
203
173
  white-space: nowrap;
204
174
  text-overflow: ellipsis;
205
- transition: max-width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
206
-
207
- @include reduced-motion {
208
- transition: none;
209
- }
210
175
  }
211
176
 
212
177
  &-icon {
213
- margin-right: 5px;
214
- font-size: 16px;
215
- flex-shrink: 0;
178
+ margin-right: $router-tab-icon-margin;
179
+ font-size: $router-tab-icon-size;
216
180
  }
217
181
 
218
182
  &:hover,
219
183
  &.is-active {
220
- color: $primary;
184
+ color: var(--router-tab-active-border);
221
185
 
222
186
  &.is-closable {
223
- padding: 0 ($tab-padding - math.div($close-icon-size + $close-icon-margin, 2));
187
+ padding: 0
188
+ calc(
189
+ var(--router-tab-padding, #{$router-tab-padding}) -
190
+ (#{$router-tab-close-icon-size} + #{$router-tab-close-icon-margin}) / 2
191
+ );
224
192
  }
225
193
 
226
194
  .router-tab__item-close {
227
- width: $close-icon-size;
228
- margin-left: $close-icon-margin;
195
+ width: $router-tab-close-icon-size;
196
+ margin-left: $router-tab-close-icon-margin;
229
197
 
230
198
  &::before,
231
199
  &::after {
232
- background-color: #fff;
200
+ background-color: currentColor;
233
201
  }
234
202
  }
235
203
  }
236
204
 
237
- &:hover {
238
- .router-tab__item-title {
239
- max-width: var(--router-tab-title-hover-max-width, 220px);
240
- }
241
- }
242
-
243
205
  &.is-active {
244
- border-bottom-color: $bg;
245
- // Use rgba with Vuetify primary color variable
246
- background: rgba(var(--v-theme-primary, 99, 91, 255), 0.08);
247
- box-shadow: inset 0 -2px 0 rgba(var(--v-theme-primary, 99, 91, 255), 0.5);
248
-
249
- // Fallback for browsers with color-mix support
250
- @supports (background: color-mix(in srgb, red 50%, blue)) {
251
- background: color-mix(in srgb, $primary 8%, $bg 92%);
252
- box-shadow: inset 0 -2px 0 color-mix(in srgb, $primary 50%, transparent);
253
- }
254
-
255
- &::after {
256
- content: '';
257
- position: absolute;
258
- left: 16px;
259
- right: 16px;
260
- bottom: 6px;
261
- height: 2px;
262
- border-radius: 999px;
263
- background: $primary;
264
- opacity: 0.7;
265
- pointer-events: none;
266
- }
267
- }
268
-
269
- &[data-title] {
270
- &::after {
271
- content: attr(data-title);
272
- position: absolute;
273
- left: 50%;
274
- bottom: calc(100% + 8px);
275
- display: inline-block;
276
- max-width: 320px;
277
- padding: 6px 12px;
278
- border-radius: 6px;
279
- background: $tooltip-bg;
280
- color: $tooltip-fg;
281
- font-size: 12px;
282
- line-height: 1.4;
283
- text-align: center;
284
- white-space: normal;
285
- word-break: break-word;
286
- transform: translate(-50%, 4px);
287
- box-shadow: $tooltip-shadow;
288
- opacity: 0;
289
- pointer-events: none;
290
- transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
291
- transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
292
- z-index: 10;
293
-
294
- @include reduced-motion {
295
- transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
296
- transform: translate(-50%, 0);
297
- }
298
- }
299
-
300
- &::before {
301
- content: '';
302
- position: absolute;
303
- left: 50%;
304
- bottom: calc(100% + 4px);
305
- width: 8px;
306
- height: 8px;
307
- background: $tooltip-bg;
308
- transform: translate(-50%, 6px) rotate(45deg);
309
- opacity: 0;
310
- pointer-events: none;
311
- transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
312
- transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
313
- z-index: 9;
314
-
315
- @include reduced-motion {
316
- transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
317
- transform: translate(-50%, 0) rotate(45deg);
318
- }
319
- }
320
-
321
- &:hover::after {
322
- opacity: 1;
323
- transform: translate(-50%, 0);
324
- }
325
-
326
- &:hover::before {
327
- opacity: 1;
328
- transform: translate(-50%, 0) rotate(45deg);
329
- }
206
+ background-color: var(--router-tab-background);
207
+ color: var(--router-tab-active-border);
208
+ border-bottom: 2px solid var(--router-tab-active-border);
209
+ font-weight: 510;
330
210
  }
331
211
 
332
212
  &-close {
@@ -335,25 +215,14 @@ $close-icon-size: 13px;
335
215
  position: relative;
336
216
  display: block;
337
217
  width: 0;
338
- height: $close-icon-size;
218
+ height: $router-tab-close-icon-size;
339
219
  margin-left: 0;
340
220
  overflow: hidden;
341
221
  border-radius: 50%;
342
222
  cursor: pointer;
343
- transition: $tab-trans;
223
+ transition: var(--router-tab-transition, $router-tab-transition);
344
224
  background: transparent;
345
225
  border: none;
346
- flex-shrink: 0;
347
-
348
- @include reduced-motion {
349
- transition: none;
350
- }
351
-
352
- // Keyboard focus
353
- &:focus-visible {
354
- outline: 2px solid $primary;
355
- outline-offset: 2px;
356
- }
357
226
 
358
227
  &::before,
359
228
  &::after {
@@ -365,12 +234,8 @@ $close-icon-size: 13px;
365
234
  height: 1px;
366
235
  margin-left: math.div(-$inner, 2);
367
236
  background-color: currentColor;
368
- transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
369
- content: '';
370
-
371
- @include reduced-motion {
372
- transition: none;
373
- }
237
+ transition: background-color 0.2s ease-in-out;
238
+ content: "";
374
239
  }
375
240
 
376
241
  &::before {
@@ -382,17 +247,11 @@ $close-icon-size: 13px;
382
247
  }
383
248
 
384
249
  &:hover {
385
- // Use Vuetify primary color with rgba
386
- background-color: rgba(var(--v-theme-primary, 99, 91, 255), 0.15);
387
-
388
- // Fallback for browsers with color-mix support
389
- @supports (background: color-mix(in srgb, red 50%, blue)) {
390
- background-color: color-mix(in srgb, $primary 15%, transparent);
391
- }
250
+ background-color: color-mix(in srgb, var(--router-tab-primary) 40%, transparent 60%);
392
251
 
393
252
  &::before,
394
253
  &::after {
395
- background-color: $primary;
254
+ background-color: #fff;
396
255
  }
397
256
  }
398
257
  }
@@ -401,106 +260,88 @@ $close-icon-size: 13px;
401
260
  &__contextmenu {
402
261
  position: fixed;
403
262
  z-index: 1000;
404
- min-width: 140px;
405
- padding: 8px 0;
406
- font-size: $font-size;
407
- background: $bg;
408
- border: 1px solid $border;
409
- box-shadow: 0 5px 15px -3px rgba(var(--v-shadow-key-umbra-opacity, 0, 0, 0), 0.2),
410
- 0 8px 20px 1px rgba(var(--v-shadow-key-penumbra-opacity, 0, 0, 0), 0.14),
411
- 0 3px 24px 2px rgba(var(--v-shadow-key-ambient-opacity, 0, 0, 0), 0.12);
412
- border-radius: 8px;
413
-
414
- a,
415
- button {
263
+ min-width: $router-tab-contextmenu-min-width;
264
+ padding: $router-tab-contextmenu-padding;
265
+ font-size: var(--router-tab-font-size, $router-tab-font-size);
266
+ background: var(--router-tab-background);
267
+ border: 1px solid var(--router-tab-border);
268
+ box-shadow: $router-tab-contextmenu-shadow;
269
+ border-radius: $router-tab-contextmenu-border-radius;
270
+
271
+ &-item {
416
272
  display: block;
417
273
  width: 100%;
418
- padding: 0 16px;
419
- line-height: 32px;
274
+ padding: $router-tab-contextmenu-item-padding;
275
+ line-height: $router-tab-contextmenu-item-height;
420
276
  text-align: left;
277
+ text-decoration: none;
421
278
  background: transparent;
422
279
  border: none;
423
280
  color: inherit;
424
281
  cursor: pointer;
425
282
  font: inherit;
426
- transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
427
- color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
428
-
429
- @include reduced-motion {
430
- transition: none;
431
- }
432
-
433
- // Keyboard focus
434
- &:focus-visible {
435
- outline: 2px solid $primary;
436
- outline-offset: -2px;
437
- }
283
+ transition: background-color 0.2s ease-in-out;
284
+ border-radius: calc(#{$router-tab-contextmenu-border-radius} - 4px);
438
285
 
439
- &[aria-disabled='true'] {
440
- color: rgba(var(--v-theme-on-surface, 148, 163, 184), 0.4);
286
+ &[aria-disabled="true"] {
287
+ color: rgba(148, 163, 184, 0.6);
441
288
  pointer-events: none;
442
289
  cursor: not-allowed;
443
290
  }
444
291
 
445
- &:hover:not([aria-disabled='true']),
446
- &:focus-visible:not([aria-disabled='true']) {
447
- background: rgba(var(--v-theme-primary, 99, 91, 255), 0.08);
448
- color: $primary;
449
-
450
- @supports (background: color-mix(in srgb, red 50%, blue)) {
451
- background: color-mix(in srgb, $primary 8%, transparent);
452
- }
292
+ &:hover:not([aria-disabled="true"]),
293
+ &:focus-visible {
294
+ background: color-mix(in srgb, var(--router-tab-primary) 10%, transparent 90%);
295
+ color: var(--router-tab-primary);
453
296
  }
454
297
  }
455
298
  }
456
- }
457
299
 
458
- .router-tab__container {
459
- padding: 1rem;
460
- position: relative;
461
- flex: 1 1 auto;
462
- background-color: transparent;
300
+ &__container {
301
+ position: relative;
302
+ flex: 1 1 auto;
303
+ background-color: var(--router-tab-background);
304
+ overflow: hidden;
305
+ }
306
+ }
307
+ .router-tab__item.is-active:after {
308
+ display: none !important;
463
309
  }
464
310
 
465
311
  .router-tab__item.is-active + .router-tab__item {
466
- border-left-color: css-var(--router-tab-border, css-var(--v-border-color, css-var(--theme-border, $light-border)));
312
+ border-left-color: transparent;
467
313
  }
468
314
 
469
- // Container query support (progressive enhancement)
470
- @supports (container-type: inline-size) {
471
- .router-tab {
472
- container-type: inline-size;
473
- container-name: router-tab;
474
- }
475
-
476
- @container router-tab (max-width: 600px) {
477
- .router-tab__item {
478
- padding: 0 12px;
315
+ // Transition animations
316
+ .router-tab-zoom-enter-active,
317
+ .router-tab-zoom-leave-active {
318
+ transition: all 0.3s ease;
319
+ }
479
320
 
480
- &-title {
481
- max-width: var(--router-tab-title-max-width, 80px);
482
- }
321
+ .router-tab-zoom-enter-from,
322
+ .router-tab-zoom-leave-to {
323
+ opacity: 0;
324
+ transform: scale(0.8);
325
+ }
483
326
 
484
- &:hover .router-tab__item-title {
485
- max-width: var(--router-tab-title-hover-max-width, 150px);
486
- }
487
- }
488
- }
327
+ .router-tab-swap-enter-active,
328
+ .router-tab-swap-leave-active {
329
+ transition:
330
+ opacity 0.2s ease,
331
+ transform 0.2s ease;
489
332
  }
490
333
 
491
- // High contrast mode support
492
- @media (prefers-contrast: high) {
493
- .router-tab {
494
- &__item {
495
- border-width: 2px;
334
+ .router-tab-swap-enter-from {
335
+ opacity: 0;
336
+ transform: translateX(10px);
337
+ }
496
338
 
497
- &.is-active {
498
- border-bottom-width: 3px;
499
- }
339
+ .router-tab-swap-leave-to {
340
+ opacity: 0;
341
+ transform: translateX(-10px);
342
+ }
500
343
 
501
- &:focus-visible {
502
- outline-width: 3px;
503
- }
504
- }
505
- }
506
- }
344
+ .router-tab-page {
345
+ width: 100%;
346
+ height: 100%;
347
+ }
@@ -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: 16px !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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue3-router-tab",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",