picocrank 1.26.0 → 1.27.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.
@@ -1,30 +1,50 @@
1
1
  <template>
2
- <div class="login-container">
2
+ <div class="login-container" :class="{ 'local-only': localOnly }">
3
3
  <Tabs :tabs="loginTabs" @tab-change="onTabChange">
4
4
  <!-- Local Login Tab -->
5
5
  <template #tab-local>
6
6
  <div class="login-section">
7
- <form @submit.prevent="handleLocalLogin" class="local-login-form">
8
- <input
9
- id="username"
10
- type="text"
11
- v-model="localLogin.username"
12
- required
13
- autocomplete="username"
14
- placeholder="Username"
15
- />
16
- <input
17
- id="password"
18
- type="password"
19
- v-model="localLogin.password"
20
- required
21
- autocomplete="current-password"
22
- placeholder="Password"
23
- />
7
+ <div
8
+ v-if="localLoginLoading"
9
+ class="local-login-loading"
10
+ role="status"
11
+ aria-live="polite"
12
+ >
13
+ <span class="local-login-spinner" aria-hidden="true" />
14
+ <p>{{ localLoginLoadingMessage }}</p>
15
+ </div>
16
+ <form v-else @submit.prevent="handleLocalLogin" class="local-login-form">
17
+ <FormField label="Username" for="username" label-skip v-model:error="usernameError">
18
+ <input
19
+ id="username"
20
+ type="text"
21
+ v-model="localLogin.username"
22
+ required
23
+ autocomplete="username"
24
+ placeholder="Username"
25
+ aria-label="Username"
26
+ />
27
+ </FormField>
28
+ <FormField label="Password" for="password" label-skip v-model:error="passwordError">
29
+ <input
30
+ id="password"
31
+ type="password"
32
+ v-model="localLogin.password"
33
+ required
34
+ autocomplete="current-password"
35
+ placeholder="Password"
36
+ aria-label="Password"
37
+ />
38
+ </FormField>
24
39
  <button type="submit" class="good">Login</button>
25
- <div v-if="localLoginError" class="error-message">
26
- {{ localLoginError }}
27
- </div>
40
+ <NotificationBlock
41
+ v-if="formError"
42
+ class="login-form-error"
43
+ type="bad"
44
+ label="Error"
45
+ :message="formError"
46
+ role="alert"
47
+ />
28
48
  </form>
29
49
  </div>
30
50
  </template>
@@ -70,6 +90,8 @@
70
90
  <script setup>
71
91
  import { ref, reactive, watch, computed, useSlots } from 'vue';
72
92
  import Tabs from './Tabs.vue';
93
+ import FormField from './FormField.vue';
94
+ import NotificationBlock from './NotificationBlock.vue';
73
95
  import { HugeiconsIcon } from '@hugeicons/vue';
74
96
 
75
97
  const props = defineProps({
@@ -95,6 +117,15 @@ const props = defineProps({
95
117
  showDefaultTabs: {
96
118
  type: Boolean,
97
119
  default: true
120
+ },
121
+ // Username & password only — no OAuth or custom tabs; hides the tab bar
122
+ localOnly: {
123
+ type: Boolean,
124
+ default: false
125
+ },
126
+ loadingMessage: {
127
+ type: String,
128
+ default: 'Signing in…'
98
129
  }
99
130
  });
100
131
 
@@ -119,8 +150,9 @@ const loginTabs = ref([]);
119
150
  function initializeTabs() {
120
151
  const tabs = [];
121
152
 
122
- // Add default tabs if enabled
123
- if (props.showDefaultTabs) {
153
+ if (props.localOnly) {
154
+ tabs.push({ id: 'local', label: props.tabLabels.local || 'Username & Password' });
155
+ } else if (props.showDefaultTabs) {
124
156
  tabs.push(
125
157
  { id: 'local', label: props.tabLabels.local || 'Username & Password' },
126
158
  { id: 'oauth', label: props.tabLabels.oauth || 'OAuth2' }
@@ -128,7 +160,7 @@ function initializeTabs() {
128
160
  }
129
161
 
130
162
  // Add custom tabs
131
- if (props.customTabs && props.customTabs.length > 0) {
163
+ if (!props.localOnly && props.customTabs && props.customTabs.length > 0) {
132
164
  tabs.push(...props.customTabs);
133
165
  }
134
166
 
@@ -139,7 +171,7 @@ function initializeTabs() {
139
171
  initializeTabs();
140
172
 
141
173
  // Watch for changes to customTabs or showDefaultTabs
142
- watch(() => [props.customTabs, props.showDefaultTabs], () => {
174
+ watch(() => [props.customTabs, props.showDefaultTabs, props.localOnly], () => {
143
175
  initializeTabs();
144
176
  }, { deep: true });
145
177
 
@@ -149,7 +181,26 @@ const localLogin = reactive({
149
181
  password: ''
150
182
  });
151
183
 
152
- const localLoginError = ref('');
184
+ const usernameError = ref('');
185
+ const passwordError = ref('');
186
+ const formError = ref('');
187
+ const localLoginLoading = ref(false);
188
+ const localLoginLoadingMessage = ref('');
189
+
190
+ function clearLocalLoginErrors() {
191
+ usernameError.value = '';
192
+ passwordError.value = '';
193
+ formError.value = '';
194
+ }
195
+
196
+ watch(
197
+ () => [localLogin.username, localLogin.password],
198
+ () => {
199
+ if (formError.value) {
200
+ formError.value = '';
201
+ }
202
+ },
203
+ );
153
204
 
154
205
  // OAuth providers - provided by parent component
155
206
  const oauthProviders = ref(props.oauthProviders);
@@ -163,9 +214,10 @@ watch(() => props.oauthProviders, (newProviders) => {
163
214
 
164
215
  // Handle local login
165
216
  function handleLocalLogin() {
166
- localLoginError.value = '';
167
-
168
- // Emit event for parent component to handle
217
+ clearLocalLoginErrors();
218
+ localLoginLoading.value = true;
219
+ localLoginLoadingMessage.value = props.loadingMessage;
220
+
169
221
  emit('local-login', {
170
222
  username: localLogin.username,
171
223
  password: localLogin.password
@@ -176,7 +228,8 @@ function handleLocalLogin() {
176
228
  function resetLocalForm() {
177
229
  localLogin.username = '';
178
230
  localLogin.password = '';
179
- localLoginError.value = '';
231
+ clearLocalLoginErrors();
232
+ localLoginLoading.value = false;
180
233
  }
181
234
 
182
235
  // Handle OAuth login
@@ -189,15 +242,28 @@ function handleOAuthLogin(provider) {
189
242
 
190
243
  // Handle tab change
191
244
  function onTabChange(tab, tabId) {
192
- // Clear errors when switching tabs
193
- localLoginError.value = '';
245
+ clearLocalLoginErrors();
194
246
  oauthError.value = '';
247
+ localLoginLoading.value = false;
195
248
  emit('tab-change', tab, tabId);
196
249
  }
197
250
 
198
- // Method to set error messages
251
+ function setLocalLoginErrors(errors = {}) {
252
+ localLoginLoading.value = false;
253
+ usernameError.value = errors.username || '';
254
+ passwordError.value = errors.password || '';
255
+ formError.value = errors.form || '';
256
+ }
257
+
199
258
  function setLocalLoginError(message) {
200
- localLoginError.value = message;
259
+ setLocalLoginErrors({ form: message });
260
+ }
261
+
262
+ function setLocalLoginLoading(loading, message) {
263
+ localLoginLoading.value = loading;
264
+ if (loading) {
265
+ localLoginLoadingMessage.value = message ?? props.loadingMessage;
266
+ }
201
267
  }
202
268
 
203
269
  function setOAuthError(message) {
@@ -214,6 +280,8 @@ defineExpose({
214
280
  addOAuthProvider,
215
281
  resetLocalForm,
216
282
  setLocalLoginError,
283
+ setLocalLoginErrors,
284
+ setLocalLoginLoading,
217
285
  setOAuthError,
218
286
  localLogin
219
287
  });
@@ -225,6 +293,15 @@ defineExpose({
225
293
  margin: 2rem auto;
226
294
  }
227
295
 
296
+ .login-container.local-only {
297
+ margin-top: 0;
298
+ margin-bottom: 0;
299
+ }
300
+
301
+ .login-container.local-only :deep(.tabs-header) {
302
+ display: none;
303
+ }
304
+
228
305
  .login-section {
229
306
  display: flex;
230
307
  flex-direction: column;
@@ -239,17 +316,46 @@ defineExpose({
239
316
  width: 100%;
240
317
  }
241
318
 
242
- .local-login-form input {
243
- padding: 0.75rem;
244
- border-radius: 4px;
245
- font-size: 1em;
319
+ .local-login-form :deep(.form-field-control) {
320
+ width: 100%;
321
+ }
322
+
323
+ .local-login-form :deep(input) {
246
324
  width: 100%;
247
325
  box-sizing: border-box;
248
326
  }
249
327
 
250
- .local-login-form input:focus-visible {
251
- outline: 2px solid var(--login-input-focus-outline);
252
- outline-offset: 2px;
328
+ .local-login-loading {
329
+ display: flex;
330
+ flex-direction: column;
331
+ align-items: center;
332
+ justify-content: center;
333
+ gap: 1rem;
334
+ width: 100%;
335
+ min-height: 9rem;
336
+ padding: 1rem 0;
337
+ color: var(--login-muted-fg);
338
+ text-align: center;
339
+ }
340
+
341
+ .local-login-loading p {
342
+ margin: 0;
343
+ font-size: 0.95em;
344
+ }
345
+
346
+ .local-login-spinner {
347
+ width: 2rem;
348
+ height: 2rem;
349
+ border: 3px solid var(--login-spinner-track);
350
+ border-top-color: var(--login-spinner-fg);
351
+ border-radius: 50%;
352
+ animation: local-login-spin 0.75s linear infinite;
353
+ }
354
+
355
+ @keyframes local-login-spin {
356
+ to {
357
+ transform: rotate(360deg);
358
+ }
253
359
  }
254
360
 
255
361
  .local-login-form button {
@@ -266,6 +372,11 @@ defineExpose({
266
372
  opacity: 0.9;
267
373
  }
268
374
 
375
+ .local-login-form :deep(.login-form-error) {
376
+ width: 100%;
377
+ font-size: 0.9em;
378
+ }
379
+
269
380
  .oauth-providers {
270
381
  display: flex;
271
382
  flex-direction: column;
@@ -185,19 +185,20 @@ watch(() => props.pageSize, (newSize) => {
185
185
 
186
186
  .button:disabled {
187
187
  background: transparent;
188
+ color: var(--button-transparent-disabled-fg);
188
189
  cursor: not-allowed;
189
190
  }
190
191
 
192
+ .button.active:disabled {
193
+ color: var(--text-color);
194
+ }
195
+
191
196
  #page-size {
192
197
  background: transparent;
193
198
  margin-left: 0.5rem;
194
199
  margin-right: 0.5rem;
195
200
  }
196
201
 
197
- option {
198
- background: #545f69;
199
- }
200
-
201
202
  /* Responsive design */
202
203
  @media (max-width: 768px) {
203
204
  .pagination {
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { nextTick } from 'vue'
4
+ import Tabs from './Tabs.vue'
5
+
6
+ const globalStubs = {
7
+ HugeiconsIcon: true,
8
+ }
9
+
10
+ describe('Tabs disabled support', () => {
11
+ const tabs = [
12
+ { id: 'first', label: 'First' },
13
+ { id: 'second', label: 'Second', disabled: true },
14
+ { id: 'third', label: 'Third' },
15
+ ]
16
+
17
+ function mountTabs(overrides = {}) {
18
+ return mount(Tabs, {
19
+ attachTo: document.body,
20
+ props: {
21
+ tabs,
22
+ defaultTab: 'first',
23
+ ...overrides.props,
24
+ },
25
+ slots: {
26
+ 'tab-first': '<p>First panel</p>',
27
+ 'tab-second': '<p>Second panel</p>',
28
+ 'tab-third': '<p>Third panel</p>',
29
+ },
30
+ global: {
31
+ stubs: globalStubs,
32
+ },
33
+ })
34
+ }
35
+
36
+ it('marks disabled tabs with disabled and aria-disabled', () => {
37
+ const wrapper = mountTabs()
38
+
39
+ const disabledTab = wrapper.find('#tab-second')
40
+ expect(disabledTab.attributes('disabled')).toBeDefined()
41
+ expect(disabledTab.attributes('aria-disabled')).toBe('true')
42
+ })
43
+
44
+ it('does not select a disabled tab on click', async () => {
45
+ const wrapper = mountTabs()
46
+
47
+ await wrapper.find('#tab-second').trigger('click')
48
+ await nextTick()
49
+
50
+ expect(wrapper.find('#tab-first').attributes('aria-selected')).toBe('true')
51
+ expect(wrapper.emitted('tab-change')).toBeUndefined()
52
+ })
53
+
54
+ it('skips disabled tabs when moving focus with arrow keys', async () => {
55
+ const wrapper = mountTabs()
56
+
57
+ await wrapper.find('#tab-first').trigger('keydown', { key: 'ArrowRight' })
58
+ await nextTick()
59
+
60
+ expect(wrapper.find('#tab-third').attributes('tabindex')).toBe('0')
61
+ expect(wrapper.find('#tab-third').attributes('aria-selected')).toBe('true')
62
+ })
63
+
64
+ it('selects the first enabled tab when defaultTab is disabled', async () => {
65
+ const wrapper = mountTabs({
66
+ props: {
67
+ defaultTab: 'second',
68
+ },
69
+ })
70
+
71
+ await nextTick()
72
+
73
+ expect(wrapper.find('#tab-first').attributes('aria-selected')).toBe('true')
74
+ })
75
+
76
+ it('moves off a tab when it becomes disabled', async () => {
77
+ const wrapper = mountTabs({
78
+ props: {
79
+ tabs: [
80
+ { id: 'first', label: 'First' },
81
+ { id: 'second', label: 'Second' },
82
+ ],
83
+ defaultTab: 'second',
84
+ },
85
+ slots: {
86
+ 'tab-first': '<p>First panel</p>',
87
+ 'tab-second': '<p>Second panel</p>',
88
+ },
89
+ })
90
+
91
+ await wrapper.setProps({
92
+ tabs: [
93
+ { id: 'first', label: 'First' },
94
+ { id: 'second', label: 'Second', disabled: true },
95
+ ],
96
+ })
97
+ await nextTick()
98
+
99
+ expect(wrapper.find('#tab-first').attributes('aria-selected')).toBe('true')
100
+ })
101
+ })
@@ -18,11 +18,14 @@
18
18
  v-for="(tab, index) in tabs"
19
19
  :key="getTabKey(tab, index)"
20
20
  :ref="(el) => setTabRef(getTabKey(tab, index), el)"
21
- :class="['tab-button', { active: isTabSelected(tab, index) }]"
21
+ :class="['tab-button', { active: isTabSelected(tab, index), disabled: isTabDisabled(tab) }]"
22
+ :disabled="isTabDisabled(tab)"
22
23
  :aria-selected="isTabSelected(tab, index) ? 'true' : 'false'"
24
+ :aria-disabled="isTabDisabled(tab) ? 'true' : undefined"
23
25
  :aria-controls="`tab-panel-${getTabKey(tab, index)}`"
24
26
  :id="`tab-${getTabKey(tab, index)}`"
25
- :tabindex="isTabFocused(tab, index) ? 0 : -1"
27
+ :tabindex="tabTabIndex(tab, index)"
28
+ :title="tab.disabledTitle || undefined"
26
29
  role="tab"
27
30
  type="button"
28
31
  @click="onTabClick(getTabKey(tab, index))"
@@ -104,18 +107,66 @@ const emit = defineEmits(['tab-change'])
104
107
 
105
108
  const tabRefs = new Map()
106
109
 
107
- function getInitialTabId() {
108
- if (props.defaultTab !== null) {
109
- return props.defaultTab
110
+ const activeTabId = ref(null)
111
+ const focusedTabId = ref(null)
112
+
113
+ function isTabDisabled(tab) {
114
+ return tab.disabled === true
115
+ }
116
+
117
+ function getFirstEnabledIndex() {
118
+ return props.tabs.findIndex((tab) => !isTabDisabled(tab))
119
+ }
120
+
121
+ function getLastEnabledIndex() {
122
+ for (let index = props.tabs.length - 1; index >= 0; index -= 1) {
123
+ if (!isTabDisabled(props.tabs[index])) {
124
+ return index
125
+ }
126
+ }
127
+ return -1
128
+ }
129
+
130
+ function getFirstEnabledTabKey() {
131
+ const index = getFirstEnabledIndex()
132
+ if (index < 0) {
133
+ return null
110
134
  }
111
- if (props.tabs.length > 0) {
112
- return getTabKey(props.tabs[0], 0)
135
+ return getTabKey(props.tabs[index], index)
136
+ }
137
+
138
+ function resolveSelectableTabId(tabKey) {
139
+ if (tabKey !== null && tabKey !== undefined) {
140
+ const index = props.tabs.findIndex((tab, i) => getTabKey(tab, i) === tabKey)
141
+ if (index >= 0 && !isTabDisabled(props.tabs[index])) {
142
+ return tabKey
143
+ }
144
+ }
145
+ return getFirstEnabledTabKey()
146
+ }
147
+
148
+ function tabTabIndex(tab, index) {
149
+ if (isTabDisabled(tab)) {
150
+ return -1
113
151
  }
114
- return null
152
+ return isTabFocused(tab, index) ? 0 : -1
115
153
  }
116
154
 
117
- const activeTabId = ref(getInitialTabId())
118
- const focusedTabId = ref(getInitialTabId())
155
+ function findNextEnabledIndex(fromIndex, step) {
156
+ const length = props.tabs.length
157
+ if (length === 0) {
158
+ return fromIndex
159
+ }
160
+
161
+ for (let offset = 1; offset <= length; offset += 1) {
162
+ const index = (fromIndex + step * offset + length * 100) % length
163
+ if (!isTabDisabled(props.tabs[index])) {
164
+ return index
165
+ }
166
+ }
167
+
168
+ return fromIndex
169
+ }
119
170
 
120
171
  function getTabKey(tab, index) {
121
172
  return tab.id ?? index
@@ -146,10 +197,14 @@ function focusTab(tabKey) {
146
197
  }
147
198
 
148
199
  function selectTab(tabKey, { emitChange = true } = {}) {
200
+ const index = tabIndexForKey(tabKey)
201
+ if (index >= 0 && isTabDisabled(props.tabs[index])) {
202
+ return
203
+ }
204
+
149
205
  if (activeTabId.value !== tabKey) {
150
206
  activeTabId.value = tabKey
151
207
  if (emitChange) {
152
- const index = tabIndexForKey(tabKey)
153
208
  const activeTab = index >= 0 ? props.tabs[index] : null
154
209
  emit('tab-change', activeTab, tabKey)
155
210
  }
@@ -166,8 +221,11 @@ function moveFocusToIndex(index, { activate = props.activation === 'auto' } = {}
166
221
  return
167
222
  }
168
223
 
169
- const normalized = (index + props.tabs.length) % props.tabs.length
170
- const tabKey = getTabKey(props.tabs[normalized], normalized)
224
+ if (index < 0 || index >= props.tabs.length || isTabDisabled(props.tabs[index])) {
225
+ return
226
+ }
227
+
228
+ const tabKey = getTabKey(props.tabs[index], index)
171
229
  focusedTabId.value = tabKey
172
230
 
173
231
  if (activate) {
@@ -177,6 +235,14 @@ function moveFocusToIndex(index, { activate = props.activation === 'auto' } = {}
177
235
  nextTick(() => focusTab(tabKey))
178
236
  }
179
237
 
238
+ function moveFocusByStep(fromIndex, step, { activate = props.activation === 'auto' } = {}) {
239
+ const nextIndex = findNextEnabledIndex(fromIndex, step)
240
+ if (nextIndex === fromIndex && isTabDisabled(props.tabs[fromIndex])) {
241
+ return
242
+ }
243
+ moveFocusToIndex(nextIndex, { activate })
244
+ }
245
+
180
246
  function tabKeyFromElement(tabEl) {
181
247
  const suffix = tabEl.id.slice('tab-'.length)
182
248
  const index = props.tabs.findIndex((tab, i) => String(getTabKey(tab, i)) === suffix)
@@ -204,34 +270,34 @@ function onTabListKeydown(event) {
204
270
  case 'ArrowRight':
205
271
  if (!isVertical) {
206
272
  event.preventDefault()
207
- moveFocusToIndex(index + 1)
273
+ moveFocusByStep(index, 1)
208
274
  }
209
275
  break
210
276
  case 'ArrowLeft':
211
277
  if (!isVertical) {
212
278
  event.preventDefault()
213
- moveFocusToIndex(index - 1)
279
+ moveFocusByStep(index, -1)
214
280
  }
215
281
  break
216
282
  case 'ArrowDown':
217
283
  if (isVertical) {
218
284
  event.preventDefault()
219
- moveFocusToIndex(index + 1)
285
+ moveFocusByStep(index, 1)
220
286
  }
221
287
  break
222
288
  case 'ArrowUp':
223
289
  if (isVertical) {
224
290
  event.preventDefault()
225
- moveFocusToIndex(index - 1)
291
+ moveFocusByStep(index, -1)
226
292
  }
227
293
  break
228
294
  case 'Home':
229
295
  event.preventDefault()
230
- moveFocusToIndex(0)
296
+ moveFocusToIndex(getFirstEnabledIndex())
231
297
  break
232
298
  case 'End':
233
299
  event.preventDefault()
234
- moveFocusToIndex(props.tabs.length - 1)
300
+ moveFocusToIndex(getLastEnabledIndex())
235
301
  break
236
302
  case ' ':
237
303
  case 'Enter':
@@ -260,20 +326,25 @@ function syncTabIdsAfterListChange() {
260
326
  return
261
327
  }
262
328
 
263
- const activeStillExists = tabIndexForKey(activeTabId.value) >= 0
264
- if (!activeStillExists) {
265
- const firstKey = getTabKey(props.tabs[0], 0)
329
+ const activeIndex = tabIndexForKey(activeTabId.value)
330
+ const activeIsValid = activeIndex >= 0 && !isTabDisabled(props.tabs[activeIndex])
331
+ if (!activeIsValid) {
332
+ const firstKey = getFirstEnabledTabKey()
266
333
  activeTabId.value = firstKey
267
334
  focusedTabId.value = firstKey
268
335
  return
269
336
  }
270
337
 
271
- if (tabIndexForKey(focusedTabId.value) < 0) {
338
+ const focusedIndex = tabIndexForKey(focusedTabId.value)
339
+ const focusedIsValid = focusedIndex >= 0 && !isTabDisabled(props.tabs[focusedIndex])
340
+ if (!focusedIsValid) {
272
341
  focusedTabId.value = activeTabId.value
273
342
  }
274
343
  }
275
344
 
276
345
  onMounted(() => {
346
+ activeTabId.value = resolveSelectableTabId(props.defaultTab)
347
+ focusedTabId.value = activeTabId.value
277
348
  syncTabIdsAfterListChange()
278
349
  })
279
350
 
@@ -357,7 +428,7 @@ defineExpose({
357
428
  position: relative;
358
429
  }
359
430
 
360
- .tab-button:hover:not(.active) {
431
+ .tab-button:hover:not(.active):not(:disabled) {
361
432
  color: var(--tab-hover-fg);
362
433
  background-color: var(--tab-hover-bg);
363
434
  }
@@ -368,6 +439,25 @@ defineExpose({
368
439
  border-bottom-color: transparent;
369
440
  }
370
441
 
442
+ .tab-button:disabled,
443
+ .tab-button.disabled {
444
+ cursor: not-allowed;
445
+ color: var(--disabled-text-color);
446
+ opacity: 0.72;
447
+ }
448
+
449
+ .tab-button:disabled:hover,
450
+ .tab-button.disabled:hover {
451
+ color: var(--disabled-text-color);
452
+ background-color: transparent;
453
+ }
454
+
455
+ .tab-button:disabled.active,
456
+ .tab-button.disabled.active {
457
+ background-color: var(--tab-active-bg);
458
+ color: var(--disabled-text-color);
459
+ }
460
+
371
461
  .tabs-vertical .tab-button {
372
462
  width: 100%;
373
463
  justify-content: flex-start;
@@ -426,7 +516,7 @@ defineExpose({
426
516
  z-index: 1;
427
517
  }
428
518
 
429
- .tab-button:hover:not(.active):focus-visible {
519
+ .tab-button:hover:not(.active):not(:disabled):focus-visible {
430
520
  color: var(--tab-hover-fg);
431
521
  background-color: var(--tab-hover-bg);
432
522
  }