picocrank 1.25.2 → 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.
Files changed (51) hide show
  1. package/eslint.config.js +75 -0
  2. package/package.json +17 -4
  3. package/router.js +74 -0
  4. package/styles/dark-theme.css +20 -11
  5. package/styles/picocrank-extensions.css +123 -30
  6. package/supplemental-themes/catppuccin-latte-frappe/theme.css +10 -24
  7. package/supplemental-themes/dracula-alucard/theme.css +10 -24
  8. package/supplemental-themes/gruvbox-dark-light/theme.css +10 -24
  9. package/supplemental-themes/waffles/theme.css +2 -16
  10. package/vite.config.js +5 -0
  11. package/vue/components/Breadcrumbs.vue +192 -45
  12. package/vue/components/Calendar.vue +162 -145
  13. package/vue/components/DangerZone.vue +186 -0
  14. package/vue/components/FormField.spec.js +350 -0
  15. package/vue/components/FormField.vue +236 -32
  16. package/vue/components/FormLayout.spec.js +56 -0
  17. package/vue/components/Header.vue +22 -6
  18. package/vue/components/Login.vue +152 -41
  19. package/vue/components/NavigationGrid.vue +26 -180
  20. package/vue/components/NotificationBlock.vue +120 -0
  21. package/vue/components/NotificationPopup.vue +44 -41
  22. package/vue/components/Pagination.vue +5 -4
  23. package/vue/components/ReadOnlyTextArea.vue +1 -1
  24. package/vue/components/SidebarNavLink.vue +3 -3
  25. package/vue/components/Table.vue +107 -79
  26. package/vue/components/TableColumnOptionsPopover.vue +9 -0
  27. package/vue/components/Tabs.spec.js +101 -0
  28. package/vue/components/Tabs.vue +455 -81
  29. package/vue/components/TopBar.vue +2 -2
  30. package/vue/components/form-validation.spec.js +126 -0
  31. package/vue/composables/tableColumnPriorities.js +33 -0
  32. package/vue/composables/tableFilters.js +1 -1
  33. package/vue/data/navSectionHubs.js +196 -0
  34. package/vue/data/routeBreadcrumbs.js +90 -0
  35. package/vue/examples/Admin.vue +12 -12
  36. package/vue/examples/App.vue +27 -36
  37. package/vue/examples/CalendarExample.vue +32 -22
  38. package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
  39. package/vue/examples/FormExample.vue +76 -48
  40. package/vue/examples/Login.vue +5 -3
  41. package/vue/examples/LoginExample.vue +99 -15
  42. package/vue/examples/NavSectionHub.vue +36 -0
  43. package/vue/examples/PageStructure.vue +15 -12
  44. package/vue/examples/StatusExample.vue +248 -129
  45. package/vue/examples/TableExample.vue +72 -71
  46. package/vue/examples/TabsExample.vue +236 -257
  47. package/vue/examples/ThemePreview.vue +21 -23
  48. package/vue/examples/ViewItem.vue +32 -5
  49. package/vue/examples/Welcome.vue +3 -40
  50. package/vue/test/formTestUtils.js +279 -0
  51. package/vue/test/setup.js +5 -0
@@ -1,112 +1,361 @@
1
1
  <template>
2
- <div class="tabs-container">
3
- <div class="tabs-header" role="tablist">
4
- <button
5
- v-for="(tab, index) in tabs"
6
- :key="tab.id || index"
7
- :class="['tab-button', { active: activeTabId === (tab.id || index) }]"
8
- :aria-selected="activeTabId === (tab.id || index)"
9
- :aria-controls="`tab-panel-${tab.id || index}`"
10
- :id="`tab-${tab.id || index}`"
11
- role="tab"
12
- @click="selectTab(tab.id || index)"
2
+ <div
3
+ class="tabs-container"
4
+ :class="{
5
+ 'tabs-vertical': orientation === 'vertical',
6
+ 'tabs-flush': !padding,
7
+ }"
8
+ >
9
+ <div class="tabs-header">
10
+ <div
11
+ class="tabs-header-scroll"
12
+ role="tablist"
13
+ :aria-orientation="orientation"
14
+ :aria-labelledby="tablistLabelledBy || undefined"
15
+ @keydown="onTabListKeydown"
13
16
  >
14
- <HugeiconsIcon
15
- v-if="tab.icon"
16
- :icon="tab.icon"
17
- width="1em"
18
- height="1em"
19
- />
20
- <span>{{ tab.label }}</span>
21
- <slot :name="`tab-${tab.id || index}-badge`" />
22
- </button>
17
+ <button
18
+ v-for="(tab, index) in tabs"
19
+ :key="getTabKey(tab, index)"
20
+ :ref="(el) => setTabRef(getTabKey(tab, index), el)"
21
+ :class="['tab-button', { active: isTabSelected(tab, index), disabled: isTabDisabled(tab) }]"
22
+ :disabled="isTabDisabled(tab)"
23
+ :aria-selected="isTabSelected(tab, index) ? 'true' : 'false'"
24
+ :aria-disabled="isTabDisabled(tab) ? 'true' : undefined"
25
+ :aria-controls="`tab-panel-${getTabKey(tab, index)}`"
26
+ :id="`tab-${getTabKey(tab, index)}`"
27
+ :tabindex="tabTabIndex(tab, index)"
28
+ :title="tab.disabledTitle || undefined"
29
+ role="tab"
30
+ type="button"
31
+ @click="onTabClick(getTabKey(tab, index))"
32
+ >
33
+ <HugeiconsIcon
34
+ v-if="tab.icon"
35
+ :icon="tab.icon"
36
+ width="1em"
37
+ height="1em"
38
+ aria-hidden="true"
39
+ />
40
+ <span>{{ tab.label }}</span>
41
+ <slot :name="`tab-${getTabKey(tab, index)}-badge`" />
42
+ </button>
43
+ </div>
23
44
  </div>
24
45
  <div class="tabs-content">
25
46
  <div
26
47
  v-for="(tab, index) in tabs"
27
- :key="tab.id || index"
28
- :id="`tab-panel-${tab.id || index}`"
29
- :class="['tab-panel', { active: activeTabId === (tab.id || index) }]"
48
+ :key="`panel-${getTabKey(tab, index)}`"
49
+ :id="`tab-panel-${getTabKey(tab, index)}`"
50
+ :class="['tab-panel', { active: isTabSelected(tab, index) }]"
30
51
  role="tabpanel"
31
- :aria-labelledby="`tab-${tab.id || index}`"
32
- v-show="activeTabId === (tab.id || index)"
52
+ :aria-labelledby="`tab-${getTabKey(tab, index)}`"
53
+ :hidden="!isTabSelected(tab, index)"
54
+ :tabindex="isTabSelected(tab, index) ? 0 : -1"
33
55
  >
34
- <slot :name="`tab-${tab.id || index}`" :tab="tab" />
56
+ <slot :name="`tab-${getTabKey(tab, index)}`" :tab="tab" />
35
57
  </div>
36
58
  </div>
37
59
  </div>
38
60
  </template>
39
61
 
40
62
  <script setup>
41
- import { ref, onMounted, watch } from 'vue';
42
- import { HugeiconsIcon } from '@hugeicons/vue';
63
+ import { nextTick, onMounted, ref, watch } from 'vue'
64
+ import { HugeiconsIcon } from '@hugeicons/vue'
43
65
 
44
66
  const props = defineProps({
45
67
  tabs: {
46
68
  type: Array,
47
69
  required: true,
48
70
  validator: (tabs) => {
49
- return tabs.every(tab =>
50
- typeof tab === 'object' &&
51
- (tab.id !== undefined || true) &&
52
- typeof tab.label === 'string'
53
- );
54
- }
71
+ return tabs.every(tab =>
72
+ typeof tab === 'object'
73
+ && (tab.id !== undefined || true)
74
+ && typeof tab.label === 'string',
75
+ )
76
+ },
55
77
  },
56
78
  defaultTab: {
57
79
  type: [String, Number],
58
- default: null
80
+ default: null,
81
+ },
82
+ /** APG activation: `auto` selects on focus; `manual` requires Space/Enter. */
83
+ activation: {
84
+ type: String,
85
+ default: 'auto',
86
+ validator: (value) => ['auto', 'manual'].includes(value),
87
+ },
88
+ /** Optional id of an element that labels the tablist. */
89
+ tablistLabelledBy: {
90
+ type: String,
91
+ default: '',
92
+ },
93
+ /** Tab strip layout: `horizontal` (default) or `vertical`. */
94
+ orientation: {
95
+ type: String,
96
+ default: 'horizontal',
97
+ validator: (value) => ['horizontal', 'vertical'].includes(value),
98
+ },
99
+ /** When true, panels use full inset padding; default false keeps dividers flush. */
100
+ padding: {
101
+ type: Boolean,
102
+ default: false,
103
+ },
104
+ })
105
+
106
+ const emit = defineEmits(['tab-change'])
107
+
108
+ const tabRefs = new Map()
109
+
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
+ }
59
126
  }
60
- });
127
+ return -1
128
+ }
61
129
 
62
- const emit = defineEmits(['tab-change']);
130
+ function getFirstEnabledTabKey() {
131
+ const index = getFirstEnabledIndex()
132
+ if (index < 0) {
133
+ return null
134
+ }
135
+ return getTabKey(props.tabs[index], index)
136
+ }
63
137
 
64
- // Initialize activeTabId with first tab by default
65
- function getInitialTabId() {
66
- if (props.defaultTab !== null) {
67
- return props.defaultTab;
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
+ }
68
144
  }
69
- if (props.tabs.length > 0) {
70
- return props.tabs[0].id ?? 0;
145
+ return getFirstEnabledTabKey()
146
+ }
147
+
148
+ function tabTabIndex(tab, index) {
149
+ if (isTabDisabled(tab)) {
150
+ return -1
71
151
  }
72
- return null;
152
+ return isTabFocused(tab, index) ? 0 : -1
73
153
  }
74
154
 
75
- const activeTabId = ref(getInitialTabId());
155
+ function findNextEnabledIndex(fromIndex, step) {
156
+ const length = props.tabs.length
157
+ if (length === 0) {
158
+ return fromIndex
159
+ }
76
160
 
77
- function selectTab(tabId) {
78
- if (activeTabId.value !== tabId) {
79
- activeTabId.value = tabId;
80
- const activeTab = props.tabs.find(tab => (tab.id ?? props.tabs.indexOf(tab)) === tabId);
81
- emit('tab-change', activeTab, tabId);
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
+ }
82
166
  }
167
+
168
+ return fromIndex
169
+ }
170
+
171
+ function getTabKey(tab, index) {
172
+ return tab.id ?? index
83
173
  }
84
174
 
85
- function setActiveTab(tabId) {
86
- const tab = props.tabs.find(t => (t.id ?? props.tabs.indexOf(t)) === tabId);
87
- if (tab) {
88
- selectTab(tabId);
175
+ function setTabRef(tabKey, el) {
176
+ if (el) {
177
+ tabRefs.set(tabKey, el)
178
+ return
89
179
  }
180
+ tabRefs.delete(tabKey)
90
181
  }
91
182
 
92
- // Initialize active tab on mount if not already set
93
- onMounted(() => {
94
- if (activeTabId.value === null && props.tabs.length > 0) {
95
- activeTabId.value = props.tabs[0].id ?? 0;
183
+ function tabIndexForKey(tabKey) {
184
+ return props.tabs.findIndex((tab, index) => getTabKey(tab, index) === tabKey)
185
+ }
186
+
187
+ function isTabSelected(tab, index) {
188
+ return activeTabId.value === getTabKey(tab, index)
189
+ }
190
+
191
+ function isTabFocused(tab, index) {
192
+ return focusedTabId.value === getTabKey(tab, index)
193
+ }
194
+
195
+ function focusTab(tabKey) {
196
+ tabRefs.get(tabKey)?.focus()
197
+ }
198
+
199
+ function selectTab(tabKey, { emitChange = true } = {}) {
200
+ const index = tabIndexForKey(tabKey)
201
+ if (index >= 0 && isTabDisabled(props.tabs[index])) {
202
+ return
203
+ }
204
+
205
+ if (activeTabId.value !== tabKey) {
206
+ activeTabId.value = tabKey
207
+ if (emitChange) {
208
+ const activeTab = index >= 0 ? props.tabs[index] : null
209
+ emit('tab-change', activeTab, tabKey)
210
+ }
211
+ }
212
+ focusedTabId.value = tabKey
213
+ }
214
+
215
+ function onTabClick(tabKey) {
216
+ selectTab(tabKey)
217
+ }
218
+
219
+ function moveFocusToIndex(index, { activate = props.activation === 'auto' } = {}) {
220
+ if (props.tabs.length === 0) {
221
+ return
222
+ }
223
+
224
+ if (index < 0 || index >= props.tabs.length || isTabDisabled(props.tabs[index])) {
225
+ return
226
+ }
227
+
228
+ const tabKey = getTabKey(props.tabs[index], index)
229
+ focusedTabId.value = tabKey
230
+
231
+ if (activate) {
232
+ selectTab(tabKey)
96
233
  }
97
- });
98
234
 
99
- // Watch for tabs changes
100
- watch(() => props.tabs, (newTabs) => {
101
- if (newTabs.length > 0 && activeTabId.value === null) {
102
- activeTabId.value = newTabs[0].id ?? 0;
235
+ nextTick(() => focusTab(tabKey))
236
+ }
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
+
246
+ function tabKeyFromElement(tabEl) {
247
+ const suffix = tabEl.id.slice('tab-'.length)
248
+ const index = props.tabs.findIndex((tab, i) => String(getTabKey(tab, i)) === suffix)
249
+ if (index >= 0) {
250
+ return getTabKey(props.tabs[index], index)
103
251
  }
104
- }, { immediate: true });
252
+ return suffix
253
+ }
254
+
255
+ function onTabListKeydown(event) {
256
+ const tabEl = event.target?.closest?.('[role="tab"]')
257
+ if (!tabEl || !tabEl.id?.startsWith('tab-')) {
258
+ return
259
+ }
260
+
261
+ const tabKey = tabKeyFromElement(tabEl)
262
+ const index = tabIndexForKey(tabKey)
263
+ if (index < 0) {
264
+ return
265
+ }
266
+
267
+ const isVertical = props.orientation === 'vertical'
268
+
269
+ switch (event.key) {
270
+ case 'ArrowRight':
271
+ if (!isVertical) {
272
+ event.preventDefault()
273
+ moveFocusByStep(index, 1)
274
+ }
275
+ break
276
+ case 'ArrowLeft':
277
+ if (!isVertical) {
278
+ event.preventDefault()
279
+ moveFocusByStep(index, -1)
280
+ }
281
+ break
282
+ case 'ArrowDown':
283
+ if (isVertical) {
284
+ event.preventDefault()
285
+ moveFocusByStep(index, 1)
286
+ }
287
+ break
288
+ case 'ArrowUp':
289
+ if (isVertical) {
290
+ event.preventDefault()
291
+ moveFocusByStep(index, -1)
292
+ }
293
+ break
294
+ case 'Home':
295
+ event.preventDefault()
296
+ moveFocusToIndex(getFirstEnabledIndex())
297
+ break
298
+ case 'End':
299
+ event.preventDefault()
300
+ moveFocusToIndex(getLastEnabledIndex())
301
+ break
302
+ case ' ':
303
+ case 'Enter':
304
+ if (props.activation === 'manual') {
305
+ event.preventDefault()
306
+ selectTab(tabKey)
307
+ }
308
+ break
309
+ default:
310
+ break
311
+ }
312
+ }
313
+
314
+ function setActiveTab(tabKey) {
315
+ const index = tabIndexForKey(tabKey)
316
+ if (index >= 0) {
317
+ selectTab(tabKey)
318
+ nextTick(() => focusTab(tabKey))
319
+ }
320
+ }
321
+
322
+ function syncTabIdsAfterListChange() {
323
+ if (props.tabs.length === 0) {
324
+ activeTabId.value = null
325
+ focusedTabId.value = null
326
+ return
327
+ }
328
+
329
+ const activeIndex = tabIndexForKey(activeTabId.value)
330
+ const activeIsValid = activeIndex >= 0 && !isTabDisabled(props.tabs[activeIndex])
331
+ if (!activeIsValid) {
332
+ const firstKey = getFirstEnabledTabKey()
333
+ activeTabId.value = firstKey
334
+ focusedTabId.value = firstKey
335
+ return
336
+ }
337
+
338
+ const focusedIndex = tabIndexForKey(focusedTabId.value)
339
+ const focusedIsValid = focusedIndex >= 0 && !isTabDisabled(props.tabs[focusedIndex])
340
+ if (!focusedIsValid) {
341
+ focusedTabId.value = activeTabId.value
342
+ }
343
+ }
344
+
345
+ onMounted(() => {
346
+ activeTabId.value = resolveSelectableTabId(props.defaultTab)
347
+ focusedTabId.value = activeTabId.value
348
+ syncTabIdsAfterListChange()
349
+ })
350
+
351
+ watch(() => props.tabs, () => {
352
+ syncTabIdsAfterListChange()
353
+ }, { deep: true })
105
354
 
106
355
  defineExpose({
107
356
  setActiveTab,
108
- activeTabId
109
- });
357
+ activeTabId,
358
+ })
110
359
  </script>
111
360
 
112
361
  <style scoped>
@@ -114,27 +363,55 @@ defineExpose({
114
363
  width: 100%;
115
364
  }
116
365
 
366
+ .tabs-container.tabs-vertical {
367
+ display: flex;
368
+ align-items: stretch;
369
+ gap: 0;
370
+ }
371
+
117
372
  .tabs-header {
373
+ border-bottom: 1px solid var(--tab-border);
374
+ }
375
+
376
+ .tabs-vertical .tabs-header {
377
+ flex-shrink: 0;
378
+ border-bottom: none;
379
+ border-right: 1px solid var(--tab-border);
380
+ }
381
+
382
+ .tabs-header-scroll {
118
383
  display: flex;
119
- border-bottom: 2px solid var(--tab-border);
120
384
  gap: 0.25rem;
121
385
  overflow-x: auto;
122
386
  scrollbar-width: thin;
123
387
  }
124
388
 
125
- .tabs-header::-webkit-scrollbar {
389
+ .tabs-header-scroll::-webkit-scrollbar {
126
390
  height: 4px;
127
391
  }
128
392
 
129
- .tabs-header::-webkit-scrollbar-track {
393
+ .tabs-header-scroll::-webkit-scrollbar-track {
130
394
  background: transparent;
131
395
  }
132
396
 
133
- .tabs-header::-webkit-scrollbar-thumb {
397
+ .tabs-header-scroll::-webkit-scrollbar-thumb {
134
398
  background: var(--tab-border);
135
399
  border-radius: 2px;
136
400
  }
137
401
 
402
+ .tabs-vertical .tabs-header-scroll {
403
+ flex-direction: column;
404
+ overflow-x: hidden;
405
+ overflow-y: auto;
406
+ min-width: 10rem;
407
+ max-height: 24rem;
408
+ }
409
+
410
+ .tabs-vertical .tabs-header-scroll::-webkit-scrollbar {
411
+ width: 4px;
412
+ height: auto;
413
+ }
414
+
138
415
  .tab-button {
139
416
  display: flex;
140
417
  align-items: center;
@@ -142,33 +419,108 @@ defineExpose({
142
419
  padding: 0.75rem 1.25rem;
143
420
  background: transparent;
144
421
  border: none;
145
- border-bottom: 2px solid transparent;
146
- border-radius: 0;
422
+ border-bottom: 1px solid transparent;
423
+ border-radius: 0.4em 0.4em 0 0;
147
424
  cursor: pointer;
148
425
  font-size: 1em;
149
426
  color: var(--tab-fg);
150
- transition: all 0.2s ease;
151
427
  white-space: nowrap;
152
428
  position: relative;
153
429
  }
154
430
 
155
- .tab-button:hover {
431
+ .tab-button:hover:not(.active):not(:disabled) {
156
432
  color: var(--tab-hover-fg);
157
433
  background-color: var(--tab-hover-bg);
158
434
  }
159
435
 
160
436
  .tab-button.active {
437
+ background-color: var(--tab-active-bg);
161
438
  color: var(--tab-active-fg);
162
- border-bottom-color: var(--tab-active-fg);
163
- font-weight: bold;
439
+ border-bottom-color: transparent;
164
440
  }
165
441
 
166
- .tab-button:focus {
167
- outline: 2px solid var(--tab-focus-outline-color);
168
- outline-offset: -2px;
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
+
461
+ .tabs-vertical .tab-button {
462
+ width: 100%;
463
+ justify-content: flex-start;
464
+ border-bottom: none;
465
+ border-right: 1px solid transparent;
466
+ border-radius: 0.4em 0 0 0.4em;
467
+ }
468
+
469
+ .tabs-vertical .tab-button.active {
470
+ border-right-color: transparent;
471
+ }
472
+
473
+ .tabs-vertical .tabs-content {
474
+ flex: 1;
475
+ min-width: 0;
476
+ }
477
+
478
+ .tabs-flush .tab-panel {
479
+ padding: 0;
480
+ padding-top: 1.5rem;
481
+ }
482
+
483
+ .tabs-vertical.tabs-flush .tab-panel {
484
+ padding-top: 0;
485
+ padding-left: 0.75rem;
486
+ }
487
+
488
+ .tabs-vertical.tabs-flush .tabs-header {
489
+ display: flex;
490
+ flex-direction: column;
491
+ align-self: stretch;
492
+ }
493
+
494
+ .tabs-vertical.tabs-flush .tabs-header-scroll {
495
+ flex: 1;
496
+ max-height: none;
497
+ }
498
+
499
+ .tabs-vertical.tabs-flush .tab-button {
169
500
  border-radius: 0;
170
501
  }
171
502
 
503
+ .tab-button.active:hover,
504
+ .tab-button.active:focus-visible {
505
+ background-color: var(--tab-active-hover-bg);
506
+ color: var(--tab-active-fg);
507
+ }
508
+
509
+ .tab-button:focus {
510
+ outline: none;
511
+ }
512
+
513
+ .tab-button:focus-visible {
514
+ outline: none;
515
+ box-shadow: inset 0 0 0 2px var(--focus-outline-color);
516
+ z-index: 1;
517
+ }
518
+
519
+ .tab-button:hover:not(.active):not(:disabled):focus-visible {
520
+ color: var(--tab-hover-fg);
521
+ background-color: var(--tab-hover-bg);
522
+ }
523
+
172
524
  .tabs-content {
173
525
  width: 100%;
174
526
  min-height: 200px;
@@ -177,11 +529,23 @@ defineExpose({
177
529
  .tab-panel {
178
530
  padding-block: 1.5rem;
179
531
  padding-inline: 0.75rem;
532
+ }
533
+
534
+ .tab-panel:focus {
535
+ outline: none;
536
+ }
537
+
538
+ .tab-panel:focus-visible {
539
+ outline: 2px solid var(--focus-outline-color);
540
+ outline-offset: 2px;
541
+ border-radius: 0.4em;
542
+ }
543
+
544
+ .tab-panel[hidden] {
180
545
  display: none;
181
546
  }
182
547
 
183
548
  .tab-panel.active {
184
- display: block;
185
549
  animation: fadeIn 0.2s ease-in;
186
550
  }
187
551
 
@@ -198,7 +562,7 @@ defineExpose({
198
562
 
199
563
  /* Responsive */
200
564
  @media (max-width: 768px) {
201
- .tabs-header {
565
+ .tabs-header-scroll {
202
566
  gap: 0;
203
567
  }
204
568
 
@@ -211,5 +575,15 @@ defineExpose({
211
575
  padding-block: 1rem;
212
576
  padding-inline: 0.5rem;
213
577
  }
578
+
579
+ .tabs-flush .tab-panel {
580
+ padding: 0;
581
+ padding-top: 1rem;
582
+ }
583
+
584
+ .tabs-vertical.tabs-flush .tab-panel {
585
+ padding-top: 0;
586
+ padding-left: 0.5rem;
587
+ }
214
588
  }
215
589
  </style>
@@ -97,7 +97,7 @@ function handleLinkClick(callback = null) {
97
97
  padding: 0.5em 0.75em;
98
98
  border-radius: 0.25em;
99
99
  text-decoration: none;
100
- color: var(--header-fg-color, var(--header-text-color, inherit));
100
+ color: var(--header-fg-color);
101
101
  transition: background-color 0.15s ease;
102
102
  white-space: nowrap;
103
103
  border: 0;
@@ -124,7 +124,7 @@ function handleLinkClick(callback = null) {
124
124
 
125
125
  html[data-theme="dark"] {
126
126
  .topbar-link {
127
- color: var(--header-fg-color, var(--header-text-color, #f8f9fa));
127
+ color: var(--header-fg-color);
128
128
  }
129
129
 
130
130
  .separator {