vue3-router-tab 1.1.9 → 1.2.1

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
@@ -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 `#635bff`).
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:
@@ -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 | undefined = activeId.value, closeOptions: CloseTabOptions = {}) {
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.')
@@ -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
  }
@@ -2,64 +2,46 @@
2
2
  @use "sass:color";
3
3
  @use "variables" as *;
4
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};
12
- }
13
-
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};
21
- }
22
-
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};
30
- }
5
+ // =============================================================================
6
+ // CSS Custom Properties & Theme System
7
+ // =============================================================================
8
+
9
+ :root {
10
+
11
+ // Layout variables
12
+ --router-tab-header-height: #{$router-tab-header-height};
13
+ --router-tab-padding: #{$router-tab-padding};
14
+ --router-tab-font-size: #{$router-tab-font-size};
31
15
  }
32
16
 
17
+ // =============================================================================
33
18
  // Main Component Styles
19
+ // =============================================================================
20
+
34
21
  .router-tab {
35
22
  display: flex;
36
23
  flex-direction: column;
37
24
  min-height: 300px;
38
25
  background-color: var(--router-tab-background);
39
- color: var(--router-tab-foreground);
26
+ color: var(--router-tab-text);
40
27
 
41
28
  &__header {
42
29
  position: relative;
43
- z-index: 9;
30
+ z-index: 10;
44
31
  display: flex;
45
32
  flex: none;
33
+ align-items: center;
46
34
  box-sizing: border-box;
47
- height: var(--router-tab-header-height, $router-tab-header-height);
35
+ height: var(--router-tab-header-height);
48
36
  background-color: var(--router-tab-header-bg);
49
- transition: $router-tab-transition-fast;
50
37
  border-bottom: 1px solid var(--router-tab-border);
51
- }
52
-
53
- &__slot-start,
54
- &__slot-end {
55
- display: flex;
56
- align-items: center;
38
+ transition: $router-tab-transition-fast;
57
39
  }
58
40
 
59
41
  &__scroll {
60
42
  position: relative;
61
43
  flex: 1 1 0px;
62
- height: var(--router-tab-header-height, $router-tab-header-height);
44
+ height: 100%;
63
45
  overflow: hidden;
64
46
 
65
47
  &-container {
@@ -70,40 +52,12 @@
70
52
  &.is-mobile {
71
53
  overflow-x: auto;
72
54
  overflow-y: hidden;
73
- }
74
- }
75
- }
76
-
77
- &__scrollbar {
78
- $h: 3px;
79
-
80
- position: absolute;
81
- right: 0;
82
- bottom: 0;
83
- left: 0;
84
- height: $h;
85
- background-color: rgba(0, 0, 0, 0.1);
86
- border-radius: $h;
87
- opacity: 0;
88
- transition: opacity 0.3s ease-in-out;
55
+ scrollbar-width: none;
56
+ -ms-overflow-style: none;
89
57
 
90
- .router-tab__scroll:hover &,
91
- &.is-dragging {
92
- opacity: 1;
93
- }
94
-
95
- &-thumb {
96
- position: absolute;
97
- top: 0;
98
- left: 0;
99
- height: 100%;
100
- background-color: rgba(0, 0, 0, 0.1);
101
- border-radius: $h;
102
- transition: background-color 0.3s ease-in-out;
103
-
104
- &:hover,
105
- .router-tab__scrollbar.is-dragging & {
106
- background-color: rgba(0, 0, 0, 0.2);
58
+ &::-webkit-scrollbar {
59
+ display: none;
60
+ }
107
61
  }
108
62
  }
109
63
  }
@@ -123,47 +77,59 @@
123
77
  display: flex;
124
78
  flex: none;
125
79
  align-items: center;
126
- padding: 0 var(--router-tab-padding, $router-tab-padding);
127
- color: inherit;
128
- font-size: var(--router-tab-font-size, $router-tab-font-size);
129
- transform-origin: left bottom;
130
- cursor: pointer;
131
- transition: var(--router-tab-transition, $router-tab-transition);
132
- user-select: none;
80
+ gap: 0.5rem;
81
+ padding: 0 var(--router-tab-padding);
82
+ color: var(--router-tab-text);
83
+ font-size: var(--router-tab-font-size);
133
84
  background-color: transparent;
85
+ border: none;
134
86
  border-right: 1px solid var(--router-tab-border);
87
+ cursor: pointer;
88
+ user-select: none;
89
+ transition: var(--router-tab-transition);
135
90
 
136
91
  &:first-child {
137
92
  border-left: 1px solid var(--router-tab-border);
138
93
  }
139
94
 
140
- &.is-contextmenu {
95
+ &:hover {
96
+ background-color: color-mix(in srgb, var(--router-tab-primary) 4%, transparent);
141
97
  color: var(--router-tab-primary);
142
98
  }
143
99
 
144
- // Drag and drop states
145
- &.is-dragging {
146
- opacity: $router-tab-drag-opacity;
147
- cursor: $router-tab-drag-cursor;
148
- }
149
-
150
- &.is-drag-over {
151
- background: $router-tab-drag-over-bg;
152
- transition: background 0.15s ease;
100
+ &.is-active {
101
+ background-color: var(--router-tab-active-background);
102
+ color: var(--router-tab-active-text);
103
+ font-weight: 510;
153
104
 
154
- &::before {
105
+ &::after {
155
106
  content: "";
156
107
  position: absolute;
157
- left: -2px;
158
- top: 0;
159
- bottom: 0;
160
- width: 2px;
161
- background: var(--router-tab-primary);
108
+ bottom: -1px;
109
+ left: 0;
110
+ right: 0;
111
+ height: 2px;
112
+ background-color: var(--router-tab-active-border);
113
+ }
114
+
115
+ & + .router-tab__item {
116
+ border-left-color: transparent;
162
117
  }
163
118
  }
164
119
 
165
- &[draggable="true"] {
166
- cursor: $router-tab-drag-cursor;
120
+ // Icon styling using the dedicated color
121
+ &-icon {
122
+ flex-shrink: 0;
123
+ font-size: $router-tab-icon-size;
124
+ color: var(--router-tab-icon-color);
125
+
126
+ .router-tab__item.is-active & {
127
+ color: var(--router-tab-active-text);
128
+ }
129
+
130
+ .router-tab__item:hover & {
131
+ color: currentColor;
132
+ }
167
133
  }
168
134
 
169
135
  &-title {
@@ -174,125 +140,57 @@
174
140
  text-overflow: ellipsis;
175
141
  }
176
142
 
177
- &-icon {
178
- margin-right: $router-tab-icon-margin;
179
- font-size: $router-tab-icon-size;
180
- }
181
-
182
- &:hover,
183
- &.is-active {
184
- color: var(--router-tab-active-border);
185
-
186
- &.is-closable {
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
- );
192
- }
193
-
194
- .router-tab__item-close {
195
- width: $router-tab-close-icon-size;
196
- margin-left: $router-tab-close-icon-margin;
197
-
198
- &::before,
199
- &::after {
200
- background-color: currentColor;
201
- }
202
- }
203
- }
204
-
205
- &.is-active {
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;
210
- }
211
-
143
+ // Close button using button colors
212
144
  &-close {
213
- $inner: 8px;
214
-
145
+ $close-size: $router-tab-close-icon-size;
146
+ $inner-line: 8px;
147
+
215
148
  position: relative;
216
149
  display: block;
217
150
  width: 0;
218
- height: $router-tab-close-icon-size;
219
- margin-left: 0;
151
+ height: $close-size;
152
+ flex-shrink: 0;
220
153
  overflow: hidden;
154
+ border: none;
221
155
  border-radius: 50%;
156
+ background: var(--router-tab-button-bg);
157
+ color: var(--router-tab-button-color);
222
158
  cursor: pointer;
223
- transition: var(--router-tab-transition, $router-tab-transition);
224
- background: transparent;
225
- border: none;
159
+ transition: var(--router-tab-transition);
226
160
 
227
161
  &::before,
228
162
  &::after {
229
163
  position: absolute;
230
- top: 6px;
164
+ top: 50%;
231
165
  left: 50%;
232
166
  display: block;
233
- width: $inner;
167
+ width: $inner-line;
234
168
  height: 1px;
235
- margin-left: math.div(-$inner, 2);
169
+ margin-left: math.div(-$inner-line, 2);
236
170
  background-color: currentColor;
237
171
  transition: background-color 0.2s ease-in-out;
238
172
  content: "";
239
173
  }
240
174
 
241
- &::before {
242
- transform: rotate(-45deg);
243
- }
244
-
245
- &::after {
246
- transform: rotate(45deg);
247
- }
175
+ &::before { transform: translateY(-50%) rotate(-45deg); }
176
+ &::after { transform: translateY(-50%) rotate(45deg); }
248
177
 
249
178
  &:hover {
250
- background-color: color-mix(in srgb, var(--router-tab-primary) 40%, transparent 60%);
251
-
252
- &::before,
253
- &::after {
254
- background-color: #fff;
255
- }
179
+ background: var(--router-tab-active-button-bg);
180
+ color: var(--router-tab-active-button-color);
256
181
  }
257
182
  }
258
- }
259
-
260
- &__contextmenu {
261
- position: fixed;
262
- z-index: 1000;
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 {
272
- display: block;
273
- width: 100%;
274
- padding: $router-tab-contextmenu-item-padding;
275
- line-height: $router-tab-contextmenu-item-height;
276
- text-align: left;
277
- text-decoration: none;
278
- background: transparent;
279
- border: none;
280
- color: inherit;
281
- cursor: pointer;
282
- font: inherit;
283
- transition: background-color 0.2s ease-in-out;
284
- border-radius: calc(#{$router-tab-contextmenu-border-radius} - 4px);
285
183
 
286
- &[aria-disabled="true"] {
287
- color: rgba(148, 163, 184, 0.6);
288
- pointer-events: none;
289
- cursor: not-allowed;
184
+ // Show close button on hover/active
185
+ &:hover,
186
+ &.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});
290
189
  }
291
190
 
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);
191
+ .router-tab__item-close {
192
+ width: $router-tab-close-icon-size;
193
+ margin-left: $router-tab-close-icon-margin;
296
194
  }
297
195
  }
298
196
  }
@@ -304,44 +202,44 @@
304
202
  overflow: hidden;
305
203
  }
306
204
  }
307
- .router-tab__item.is-active:after {
308
- display: none !important;
309
- }
310
-
311
- .router-tab__item.is-active + .router-tab__item {
312
- border-left-color: transparent;
313
- }
314
-
315
- // Transition animations
316
- .router-tab-zoom-enter-active,
317
- .router-tab-zoom-leave-active {
318
- transition: all 0.3s ease;
319
- }
320
205
 
321
- .router-tab-zoom-enter-from,
322
- .router-tab-zoom-leave-to {
323
- opacity: 0;
324
- transform: scale(0.8);
325
- }
326
-
327
- .router-tab-swap-enter-active,
328
- .router-tab-swap-leave-active {
329
- transition:
330
- opacity 0.2s ease,
331
- transform 0.2s ease;
332
- }
333
-
334
- .router-tab-swap-enter-from {
335
- opacity: 0;
336
- transform: translateX(10px);
337
- }
338
-
339
- .router-tab-swap-leave-to {
340
- opacity: 0;
341
- transform: translateX(-10px);
342
- }
206
+ // Context Menu using button colors
207
+ .router-tab__contextmenu {
208
+ position: fixed;
209
+ z-index: 1000;
210
+ min-width: $router-tab-contextmenu-min-width;
211
+ padding: $router-tab-contextmenu-padding;
212
+ font-size: var(--router-tab-font-size);
213
+ background: var(--router-tab-background);
214
+ border: 1px solid var(--router-tab-border);
215
+ border-radius: $router-tab-contextmenu-border-radius;
216
+ box-shadow: $router-tab-contextmenu-shadow;
217
+
218
+ &-item {
219
+ display: block;
220
+ width: 100%;
221
+ padding: $router-tab-contextmenu-item-padding;
222
+ line-height: $router-tab-contextmenu-item-height;
223
+ text-align: left;
224
+ text-decoration: none;
225
+ background: var(--router-tab-button-bg);
226
+ border: none;
227
+ color: var(--router-tab-button-color);
228
+ cursor: pointer;
229
+ font: inherit;
230
+ border-radius: calc(#{$router-tab-contextmenu-border-radius} - 4px);
231
+ transition: all 0.2s ease-in-out;
232
+
233
+ &[aria-disabled="true"] {
234
+ color: color-mix(in srgb, var(--router-tab-text) 40%);
235
+ pointer-events: none;
236
+ cursor: not-allowed;
237
+ }
343
238
 
344
- .router-tab-page {
345
- width: 100%;
346
- height: 100%;
347
- }
239
+ &:hover:not([aria-disabled="true"]),
240
+ &:focus-visible {
241
+ background: var(--router-tab-active-button-bg);
242
+ color: var(--router-tab-active-button-color);
243
+ }
244
+ }
245
+ }
@@ -26,7 +26,7 @@ $router-tab-icon-size: 14px !default;
26
26
  $router-tab-icon-margin: 6px !default;
27
27
 
28
28
  // Close icon
29
- $router-tab-close-icon-size: 16px !default;
29
+ $router-tab-close-icon-size: 12px !default;
30
30
  $router-tab-close-icon-margin: 4px !default;
31
31
 
32
32
  // Context menu
package/lib/theme.ts CHANGED
@@ -1,7 +1,6 @@
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 = '#635bff'
5
4
  const MEDIA_QUERY = '(prefers-color-scheme: dark)'
6
5
 
7
6
  let mediaListener: ((event: MediaQueryListEvent) => void) | null = null
@@ -12,11 +11,86 @@ export interface RouterTabsThemeOptions {
12
11
  defaultStyle?: 'light' | 'dark' | 'system'
13
12
  defaultPrimary?: string
14
13
  }
14
+ export interface ColorStyle {
15
+ // Core colors
16
+ primary: string;
17
+ background: string;
18
+ text: string;
19
+ border: string;
20
+
21
+ // Interactive states
22
+ activeBackground: string;
23
+ activeText: string;
24
+ activeBorder: string;
25
+
26
+ // Header specific
27
+ headerBackground: string;
28
+
29
+ // Button specific
30
+ buttonBackground: string;
31
+ buttonColor: string;
32
+ activeButtonBackground: string;
33
+ activeButtonColor: string;
34
+
35
+ // Icon specific
36
+ iconColor: string;
37
+ }
38
+
39
+ const defaultColors: ColorStyle = {
40
+ primary: "#034960",
41
+ background: "#ffffff",
42
+ text: "#1e293b",
43
+ border: "#e2e8f0",
44
+
45
+ activeBackground: "#034960",
46
+ activeText: "#ffffff",
47
+ activeBorder: "#034960",
48
+
49
+ headerBackground: "#ffff",
50
+
51
+ buttonBackground: "#f8fafc",
52
+ buttonColor: "#034960",
53
+ activeButtonBackground: "#034960",
54
+ activeButtonColor: "#ffffff",
55
+
56
+ iconColor: "#475569",
57
+ }
15
58
 
16
- function applyPrimary(color: string) {
59
+ const defaultDarkColor: ColorStyle = {
60
+ primary: "#38bdf8",
61
+ background: "#0f172a",
62
+ text: "#f1f5f9",
63
+ border: "#334155",
64
+
65
+ activeBackground: "#1e293b",
66
+ activeText: "#38bdf8",
67
+ activeBorder: "#38bdf8",
68
+
69
+ headerBackground: "#0c4a6e", // Darker shade of primary
70
+
71
+ buttonBackground: "#1e293b",
72
+ buttonColor: "#f1f5f9",
73
+ activeButtonBackground: "#38bdf8",
74
+ activeButtonColor: "#0f172a",
75
+
76
+ iconColor: "#cbd5e1",
77
+ }
78
+
79
+ function applyPrimary(color: ColorStyle) {
17
80
  if (typeof document === 'undefined') return
18
- document.documentElement.style.setProperty('--theme-primary', color)
19
- document.documentElement.style.setProperty('--router-tab-primary', color)
81
+ console.log('applyPrimary', color)
82
+ document.documentElement.style.setProperty('--router-tab-primary', color.primary ?? defaultColors.primary)
83
+ document.documentElement.style.setProperty('--router-tab-header-bg', color.headerBackground ?? defaultColors.headerBackground)
84
+ document.documentElement.style.setProperty('--router-tab-background', color.background ?? defaultColors.background)
85
+ document.documentElement.style.setProperty('--router-tab-active-background', color.activeBackground ?? defaultColors.activeBackground)
86
+ document.documentElement.style.setProperty('--router-tab-text', color.text ?? defaultColors.text)
87
+ document.documentElement.style.setProperty('--router-tab-active-text', color.activeText ?? defaultColors.activeText)
88
+ document.documentElement.style.setProperty('--router-tab-border', color.border ?? defaultColors.border)
89
+ document.documentElement.style.setProperty('--router-tab-active-border', color.activeBorder ?? defaultColors.activeBorder)
90
+ document.documentElement.style.setProperty('--router-tab-button-color', color.buttonColor ?? defaultColors.buttonColor)
91
+ document.documentElement.style.setProperty('--router-tab-active-button-color', color.activeButtonColor ?? defaultColors.activeButtonColor)
92
+ document.documentElement.style.setProperty('--router-tab-button-background', color.buttonBackground ?? defaultColors.buttonBackground)
93
+ document.documentElement.style.setProperty('--router-tab-active-button-background', color.activeButtonBackground ?? defaultColors.activeButtonBackground)
20
94
  }
21
95
 
22
96
  function applyStyle(style: 'light' | 'dark' | 'system') {
@@ -48,16 +122,17 @@ export function initRouterTabsTheme(options: RouterTabsThemeOptions = {}) {
48
122
 
49
123
  const {
50
124
  styleKey = STYLE_KEY,
51
- primaryKey = PRIMARY_KEY,
52
125
  defaultStyle = DEFAULT_STYLE,
53
- defaultPrimary = DEFAULT_PRIMARY
54
126
  } = options
55
127
 
56
128
  const storedStyle = (window.localStorage.getItem(styleKey) as 'light' | 'dark' | 'system' | null) ?? defaultStyle
57
- const storedPrimary = window.localStorage.getItem(primaryKey) ?? defaultPrimary
58
129
 
59
130
  applyStyle(storedStyle)
60
- applyPrimary(storedPrimary)
131
+ if (storedStyle === 'dark') {
132
+ applyPrimary(defaultDarkColor)
133
+ } else {
134
+ applyPrimary(defaultColors)
135
+ }
61
136
  }
62
137
 
63
138
  export function setRouterTabsTheme(style: 'light' | 'dark' | 'system', options?: RouterTabsThemeOptions) {
@@ -67,9 +142,9 @@ export function setRouterTabsTheme(style: 'light' | 'dark' | 'system', options?:
67
142
  applyStyle(style)
68
143
  }
69
144
 
70
- export function setRouterTabsPrimary(color: string, options?: RouterTabsThemeOptions) {
145
+ export function setRouterTabsPrimary(color: ColorStyle, options?: RouterTabsThemeOptions) {
71
146
  if (typeof window === 'undefined') return
72
147
  const key = options?.primaryKey ?? PRIMARY_KEY
73
- window.localStorage.setItem(key, color)
148
+ window.localStorage.setItem(key, JSON.stringify(color))
74
149
  applyPrimary(color)
75
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue3-router-tab",
3
- "version": "1.1.9",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",