@meistrari/tela-build 1.45.0 → 1.47.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/components/tela/heading-tabs/heading-tabs.mdx +99 -0
- package/components/tela/heading-tabs/heading-tabs.stories.ts +111 -0
- package/components/tela/heading-tabs/heading-tabs.vue +46 -0
- package/components/tela/preview/preview-content.vue +54 -20
- package/components/tela/preview/preview-floating-bar.vue +7 -5
- package/components/tela/preview/preview-select-variable.vue +2 -2
- package/components/tela/segment-toggle/segment-toggle.vue +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as HeadingTabsStories from './heading-tabs.stories.ts';
|
|
3
|
+
|
|
4
|
+
<Meta of={HeadingTabsStories} />
|
|
5
|
+
|
|
6
|
+
# TelaHeadingTabs
|
|
7
|
+
|
|
8
|
+
A heading-styled tab switcher. Each option renders as a heading-typography button (`md` → `heading-h3-semibold`, `lg` → `heading-h2-semibold`) — the active tab in `text-primary`, inactive tabs in `text-tertiary` with a hover transition to `text-secondary`. Use it to switch between sections or lists where the tabs double as section headings (e.g. a dashboard's "Recents / Team activity / Favorites").
|
|
9
|
+
|
|
10
|
+
This is distinct from `TelaTabs` (the small, underlined Reka-based tab bar with an indicator) and `TelaSegmentToggle` (the pill segmented control). Reach for `TelaHeadingTabs` when the tabs themselves are the page's section headings.
|
|
11
|
+
|
|
12
|
+
## Examples
|
|
13
|
+
|
|
14
|
+
### Basic Usage
|
|
15
|
+
|
|
16
|
+
```vue
|
|
17
|
+
<script setup>
|
|
18
|
+
import { ref } from 'vue'
|
|
19
|
+
|
|
20
|
+
const selectedTab = ref('recents')
|
|
21
|
+
const options = [
|
|
22
|
+
{ tab: 'recents', label: 'Recents' },
|
|
23
|
+
{ tab: 'everyone', label: 'Team activity' },
|
|
24
|
+
{ tab: 'favorites', label: 'Favorites' }
|
|
25
|
+
]
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<TelaHeadingTabs v-model="selectedTab" :options="options" />
|
|
30
|
+
</template>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Heading Sizes
|
|
34
|
+
|
|
35
|
+
```vue
|
|
36
|
+
<!-- Default (md) → heading-h3-semibold -->
|
|
37
|
+
<TelaHeadingTabs v-model="selectedTab" :options="options" />
|
|
38
|
+
|
|
39
|
+
<!-- Larger (lg) → heading-h2-semibold -->
|
|
40
|
+
<TelaHeadingTabs v-model="selectedTab" :options="options" size="lg" />
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Conditionally Hidden Tab
|
|
44
|
+
|
|
45
|
+
Use the per-option `hidden` flag instead of conditionally building the array — keeps the tab list stable and the markup declarative.
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<TelaHeadingTabs
|
|
49
|
+
v-model="selectedTab"
|
|
50
|
+
:options="[
|
|
51
|
+
{ tab: 'recents', label: 'Recents' },
|
|
52
|
+
{ tab: 'everyone', label: 'Team activity' },
|
|
53
|
+
{ tab: 'favorites', label: 'Favorites', hidden: !hasFavorites }
|
|
54
|
+
]"
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Custom Styling
|
|
59
|
+
|
|
60
|
+
```vue
|
|
61
|
+
<TelaHeadingTabs
|
|
62
|
+
v-model="selectedTab"
|
|
63
|
+
:options="options"
|
|
64
|
+
class="gap-6"
|
|
65
|
+
tab-class="uppercase"
|
|
66
|
+
/>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Props
|
|
70
|
+
|
|
71
|
+
<ArgTypes />
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
interface HeadingTabsOption {
|
|
75
|
+
tab: string
|
|
76
|
+
label: string
|
|
77
|
+
hidden?: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type HeadingTabsProps = {
|
|
81
|
+
modelValue: string
|
|
82
|
+
options: HeadingTabsOption[]
|
|
83
|
+
size?: 'md' | 'lg'
|
|
84
|
+
class?: string
|
|
85
|
+
tabClass?: string
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Events
|
|
90
|
+
|
|
91
|
+
- `update:modelValue` - Emitted when a tab is selected with the new tab value (via `v-model`)
|
|
92
|
+
|
|
93
|
+
## Features
|
|
94
|
+
|
|
95
|
+
- **Heading typography**: `md` → `heading-h3-semibold` (default), `lg` → `heading-h2-semibold`
|
|
96
|
+
- **Active/inactive states**: `text-primary` active, `text-tertiary` inactive with hover to `text-secondary`
|
|
97
|
+
- **v-model binding**: Two-way bound selected tab
|
|
98
|
+
- **Per-option visibility**: Hide tabs declaratively with `hidden`
|
|
99
|
+
- **Customizable**: Override container and per-button classes
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
import HeadingTabs from './heading-tabs.vue'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof HeadingTabs> = {
|
|
7
|
+
title: 'Core/HeadingTabs',
|
|
8
|
+
component: HeadingTabs,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'centered',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: 'A heading-styled tab switcher. Renders each option as a heading-typography button, with the active tab in `text-primary` and inactive tabs in `text-tertiary`. Useful for switching between sections or lists on a dashboard where the tabs double as section headings. Supports v-model binding, two heading sizes (`md` → `heading-h3-semibold`, `lg` → `heading-h2-semibold`), and per-option `hidden`.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
modelValue: {
|
|
19
|
+
control: 'text',
|
|
20
|
+
description: 'The currently selected tab value (v-model).',
|
|
21
|
+
},
|
|
22
|
+
options: {
|
|
23
|
+
control: 'object',
|
|
24
|
+
description: 'Array of tab options. Each option has a `tab` (value), `label`, and optional `hidden` flag.',
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
control: 'select',
|
|
28
|
+
options: ['md', 'lg'],
|
|
29
|
+
description: 'Heading size. `md` → `heading-h3-semibold` (default), `lg` → `heading-h2-semibold`.',
|
|
30
|
+
},
|
|
31
|
+
class: {
|
|
32
|
+
control: 'text',
|
|
33
|
+
description: 'Custom CSS classes applied to the tabs container.',
|
|
34
|
+
},
|
|
35
|
+
tabClass: {
|
|
36
|
+
control: 'text',
|
|
37
|
+
description: 'Custom CSS classes applied to each tab button.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
args: {
|
|
41
|
+
options: [
|
|
42
|
+
{ tab: 'recents', label: 'Recents' },
|
|
43
|
+
{ tab: 'everyone', label: 'Team activity' },
|
|
44
|
+
{ tab: 'favorites', label: 'Favorites' },
|
|
45
|
+
],
|
|
46
|
+
modelValue: 'recents',
|
|
47
|
+
size: 'md',
|
|
48
|
+
},
|
|
49
|
+
render: (args) => {
|
|
50
|
+
return {
|
|
51
|
+
components: { HeadingTabs },
|
|
52
|
+
setup() {
|
|
53
|
+
const value = ref<string>(args.modelValue || '')
|
|
54
|
+
|
|
55
|
+
watch(
|
|
56
|
+
() => args.modelValue,
|
|
57
|
+
(val) => {
|
|
58
|
+
value.value = val || ''
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return { args, value }
|
|
63
|
+
},
|
|
64
|
+
template: `
|
|
65
|
+
<div style="padding: 20px; display: flex; flex-direction: column; gap: 16px;">
|
|
66
|
+
<HeadingTabs
|
|
67
|
+
v-model="value"
|
|
68
|
+
:options="args.options"
|
|
69
|
+
:size="args.size"
|
|
70
|
+
:class="args.class"
|
|
71
|
+
:tab-class="args.tabClass"
|
|
72
|
+
/>
|
|
73
|
+
<div style="font-family: monospace; font-size: 12px;">Selected: {{ value }}</div>
|
|
74
|
+
</div>
|
|
75
|
+
`,
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default meta
|
|
81
|
+
|
|
82
|
+
type Story = StoryObj<typeof meta>
|
|
83
|
+
|
|
84
|
+
export const Default: Story = {}
|
|
85
|
+
|
|
86
|
+
export const Large: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
size: 'lg',
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const TwoTabs: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
options: [
|
|
95
|
+
{ tab: 'recents', label: 'Recents' },
|
|
96
|
+
{ tab: 'everyone', label: 'Team activity' },
|
|
97
|
+
],
|
|
98
|
+
modelValue: 'recents',
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const WithHiddenTab: Story = {
|
|
103
|
+
args: {
|
|
104
|
+
options: [
|
|
105
|
+
{ tab: 'recents', label: 'Recents' },
|
|
106
|
+
{ tab: 'everyone', label: 'Team activity' },
|
|
107
|
+
{ tab: 'favorites', label: 'Favorites', hidden: true },
|
|
108
|
+
],
|
|
109
|
+
modelValue: 'recents',
|
|
110
|
+
},
|
|
111
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Option {
|
|
5
|
+
tab: string
|
|
6
|
+
label: string
|
|
7
|
+
hidden?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<{
|
|
11
|
+
options: Option[]
|
|
12
|
+
size?: 'md' | 'lg'
|
|
13
|
+
class?: HTMLAttributes['class']
|
|
14
|
+
tabClass?: HTMLAttributes['class']
|
|
15
|
+
}>(), {
|
|
16
|
+
size: 'md',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const selectedTab = defineModel<string>({ required: true })
|
|
20
|
+
|
|
21
|
+
const headingClass = computed(() => props.size === 'lg' ? 'heading-h2-semibold' : 'heading-h3-semibold')
|
|
22
|
+
|
|
23
|
+
const visibleOptions = computed(() => props.options.filter(option => !option.hidden))
|
|
24
|
+
|
|
25
|
+
function selectTab(tab: string) {
|
|
26
|
+
selectedTab.value = tab
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div :class="cn('flex items-center gap-[12px]', props.class)">
|
|
32
|
+
<button
|
|
33
|
+
v-for="option in visibleOptions"
|
|
34
|
+
:key="option.tab"
|
|
35
|
+
type="button"
|
|
36
|
+
:class="cn(
|
|
37
|
+
headingClass,
|
|
38
|
+
selectedTab === option.tab ? 'text-primary' : 'text-tertiary duration-120 hover:text-secondary',
|
|
39
|
+
props.tabClass,
|
|
40
|
+
)"
|
|
41
|
+
@click="selectTab(option.tab)"
|
|
42
|
+
>
|
|
43
|
+
{{ option.label }}
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
@@ -85,6 +85,28 @@ const isPageInputFocused = ref(false)
|
|
|
85
85
|
const isProgrammaticScroll = ref(false)
|
|
86
86
|
|
|
87
87
|
const scale = ref(props.initialScale ?? (props.variant === 'minimal' ? 1 : 0.75))
|
|
88
|
+
|
|
89
|
+
const IMAGE_BASE_WIDTH_DEFAULT = 480
|
|
90
|
+
const IMAGE_BASE_WIDTH_MINIMAL = 256
|
|
91
|
+
const isImage = computed(() => !!props.file.fileType?.startsWith('image/'))
|
|
92
|
+
const imageBaseWidth = computed(() => (props.variant === 'minimal' ? IMAGE_BASE_WIDTH_MINIMAL : IMAGE_BASE_WIDTH_DEFAULT))
|
|
93
|
+
const imageDisplayWidth = computed(() => Math.round(imageBaseWidth.value * scale.value))
|
|
94
|
+
|
|
95
|
+
const imageWrapperStyle = computed(() => {
|
|
96
|
+
if (!isImage.value) {
|
|
97
|
+
return { width: props.variant === 'minimal' ? '256px' : '446px', maxWidth: '100%' }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (props.variant === 'minimal') {
|
|
101
|
+
return { width: `${Math.round(scale.value * 100)}%`, maxWidth: 'none' }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
width: `${imageDisplayWidth.value}px`,
|
|
106
|
+
maxWidth: scale.value > 1 ? 'none' : '100%',
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
88
110
|
const scrollContainerRef = ref<HTMLElement | null>(null)
|
|
89
111
|
const isDragging = ref(false)
|
|
90
112
|
const dragEnabled = ref(false)
|
|
@@ -545,7 +567,6 @@ function setupPdfObserver() {
|
|
|
545
567
|
pdfObserver.value.observe(el)
|
|
546
568
|
}
|
|
547
569
|
|
|
548
|
-
// Render first pages immediately — don't wait for async observer callback
|
|
549
570
|
visiblePages.value.add(1)
|
|
550
571
|
void renderVisiblePages()
|
|
551
572
|
}
|
|
@@ -669,12 +690,19 @@ watch(() => props.file, async (newFile, oldFile) => {
|
|
|
669
690
|
pdfDocHandle.value = null
|
|
670
691
|
pdfLoadError.value = null
|
|
671
692
|
|
|
693
|
+
dragEnabled.value = false
|
|
694
|
+
isDragging.value = false
|
|
695
|
+
|
|
672
696
|
if (!newFile) {
|
|
673
697
|
fileUrl.value = undefined
|
|
674
698
|
isLoading.value = false
|
|
675
699
|
return
|
|
676
700
|
}
|
|
677
701
|
|
|
702
|
+
scale.value = newFile.fileType?.startsWith('image/')
|
|
703
|
+
? (props.initialScale ?? 1)
|
|
704
|
+
: (props.initialScale ?? (props.variant === 'minimal' ? 1 : 0.75))
|
|
705
|
+
|
|
678
706
|
isLoading.value = true
|
|
679
707
|
error.value = null
|
|
680
708
|
|
|
@@ -897,8 +925,6 @@ watch([() => props.highlightText, () => props.highlightPage, () => props.highlig
|
|
|
897
925
|
if (pdfDocHandle.value) {
|
|
898
926
|
await reRenderAllPdfPages()
|
|
899
927
|
}
|
|
900
|
-
// When doc isn't loaded yet, keep pendingScrollPage — it will be
|
|
901
|
-
// consumed by reRenderAllPdfPages().finally once the PDF renders.
|
|
902
928
|
})
|
|
903
929
|
</script>
|
|
904
930
|
|
|
@@ -906,10 +932,10 @@ watch([() => props.highlightText, () => props.highlightPage, () => props.highlig
|
|
|
906
932
|
<div
|
|
907
933
|
relative
|
|
908
934
|
:data-pdf-preview="props.file.fileType === 'application/pdf'"
|
|
909
|
-
:data-has-tabs="props.segmentTab && props.file.fileType !== 'application/pdf'"
|
|
935
|
+
:data-has-tabs="props.segmentTab === 'processed' && props.file.fileType !== 'application/pdf'"
|
|
910
936
|
:class="cn('group', {
|
|
911
|
-
'data-[has-tabs=true]:pt-88px data-[pdf-preview=false]:pt-
|
|
912
|
-
'data-[has-tabs=true]:pt-42px data-[pdf-preview=false]:pt-
|
|
937
|
+
'data-[has-tabs=true]:pt-88px data-[pdf-preview=false]:pt-0px': variant === 'default',
|
|
938
|
+
'data-[has-tabs=true]:pt-42px data-[pdf-preview=false]:pt-0px': variant === 'minimal',
|
|
913
939
|
})"
|
|
914
940
|
overflow-y-auto no-scrollbar
|
|
915
941
|
@mouseenter="handleContainerMouseEnter"
|
|
@@ -1119,7 +1145,7 @@ watch([() => props.highlightText, () => props.highlightPage, () => props.highlig
|
|
|
1119
1145
|
<!-- PDF Content -->
|
|
1120
1146
|
<template v-else-if="props.file.fileType === 'application/pdf'">
|
|
1121
1147
|
<Motion
|
|
1122
|
-
relative
|
|
1148
|
+
relative mb--48px
|
|
1123
1149
|
class="group"
|
|
1124
1150
|
:initial="{ opacity: 0, filter: 'blur(4px)', pointerEvents: 'none' }"
|
|
1125
1151
|
:animate="{ opacity: 1, filter: 'blur(0px)', pointerEvents: 'auto' }"
|
|
@@ -1205,21 +1231,29 @@ watch([() => props.highlightText, () => props.highlightPage, () => props.highlig
|
|
|
1205
1231
|
|| props.file.fileType?.startsWith('video/')
|
|
1206
1232
|
|| props.file.fileType?.startsWith('audio/')"
|
|
1207
1233
|
>
|
|
1208
|
-
<div
|
|
1209
|
-
mx-auto
|
|
1210
|
-
flex items-center justify-center
|
|
1211
|
-
:class="cn('h-[calc(100%-56px)]', {
|
|
1212
|
-
'w-446px': variant === 'default',
|
|
1213
|
-
'w-256px': variant === 'minimal',
|
|
1214
|
-
})"
|
|
1215
|
-
>
|
|
1234
|
+
<div relative h-full w-full mb--48px>
|
|
1216
1235
|
<div
|
|
1217
|
-
|
|
1218
|
-
|
|
1236
|
+
ref="scrollContainerRef"
|
|
1237
|
+
absolute inset-0 select-none overflow-auto no-scrollbar flex
|
|
1238
|
+
class="[justify-content:safe_center] [align-items:safe_center]"
|
|
1239
|
+
:class="cn(
|
|
1240
|
+
variant === 'minimal' ? 'px-16px py-16px' : 'px-46px py-24px',
|
|
1241
|
+
isImage && dragEnabled ? (isDragging ? 'cursor-grabbing' : 'cursor-grab') : 'cursor-default',
|
|
1242
|
+
)"
|
|
1243
|
+
@mousedown="handleMouseDown"
|
|
1244
|
+
@mousemove="handleMouseMove"
|
|
1245
|
+
@mouseup="handleMouseUp"
|
|
1246
|
+
@mouseleave="handleMouseLeave"
|
|
1219
1247
|
>
|
|
1220
|
-
<
|
|
1221
|
-
|
|
1222
|
-
|
|
1248
|
+
<div
|
|
1249
|
+
flex-shrink-0 border-0.5px rounded-12px overflow-hidden
|
|
1250
|
+
class="border-[#000]/8 box-shadow-[0_3px_24px_0_rgba(214,218,224,0.24),0_12px_52px_0_rgba(214,218,224,0.16)]"
|
|
1251
|
+
:style="imageWrapperStyle"
|
|
1252
|
+
>
|
|
1253
|
+
<img v-if="props.file.fileType?.startsWith('image/')" :src="fileUrl!" width="100%" :alt="props.file.fileName" :draggable="false">
|
|
1254
|
+
<video v-else-if="props.file.fileType?.startsWith('video/')" :src="fileUrl!" width="100%" controls />
|
|
1255
|
+
<audio v-else-if="props.file.fileType?.startsWith('audio/')" :src="fileUrl!" controls style="width: 100%;" />
|
|
1256
|
+
</div>
|
|
1223
1257
|
</div>
|
|
1224
1258
|
</div>
|
|
1225
1259
|
</template>
|
|
@@ -57,11 +57,13 @@ onMounted(() => {
|
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
const isPdf = computed(() => props.file.fileType === 'application/pdf')
|
|
60
|
+
const isImage = computed(() => !!props.file.fileType?.startsWith('image/'))
|
|
61
|
+
const canZoom = computed(() => (isPdf.value ? props.totalPages > 0 : isImage.value))
|
|
60
62
|
|
|
61
63
|
const actions = computed(() => ({
|
|
62
|
-
zoomIn: props.variant === 'default' &&
|
|
63
|
-
zoomOut: props.variant === 'default' &&
|
|
64
|
-
handMode: props.variant === 'default' && isPdf.value && props.segmentTab !== 'processed',
|
|
64
|
+
zoomIn: props.variant === 'default' && canZoom.value && props.segmentTab !== 'processed',
|
|
65
|
+
zoomOut: props.variant === 'default' && canZoom.value && props.segmentTab !== 'processed',
|
|
66
|
+
handMode: props.variant === 'default' && (isPdf.value || isImage.value) && props.segmentTab !== 'processed',
|
|
65
67
|
fullscreen: showFullscreenButton.value,
|
|
66
68
|
}))
|
|
67
69
|
|
|
@@ -130,7 +132,7 @@ const computedWidthContent = computed(() => {
|
|
|
130
132
|
<div flex items-center justify-center gap-4px>
|
|
131
133
|
<TelaTooltipGroup>
|
|
132
134
|
<div flex items-center justify-center gap-2px>
|
|
133
|
-
<template v-if="variant === 'minimal'
|
|
135
|
+
<template v-if="variant === 'minimal'">
|
|
134
136
|
<TelaTooltipGroupTrigger variant="multiline">
|
|
135
137
|
<button
|
|
136
138
|
w-40px h-40px
|
|
@@ -303,7 +305,7 @@ const computedWidthContent = computed(() => {
|
|
|
303
305
|
</div>
|
|
304
306
|
</template>
|
|
305
307
|
|
|
306
|
-
<template v-if="variant === 'default'
|
|
308
|
+
<template v-if="variant === 'default'">
|
|
307
309
|
<div aria-hidden h-24px w-0.5px class="bg-white/16" />
|
|
308
310
|
|
|
309
311
|
<div pl-12px pr-14px>
|
|
@@ -91,9 +91,9 @@ const firstColumnWidth = computed(() => {
|
|
|
91
91
|
return '82px'
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const minWidth =
|
|
94
|
+
const minWidth = 56
|
|
95
95
|
const maxLength = Math.max(...allTexts.map(text => text.length))
|
|
96
|
-
const charWidth = 7
|
|
96
|
+
const charWidth = 7
|
|
97
97
|
|
|
98
98
|
const width = Math.max(minWidth, maxLength * charWidth)
|
|
99
99
|
|
|
@@ -28,7 +28,7 @@ function selectTab(value: string) {
|
|
|
28
28
|
|
|
29
29
|
<template>
|
|
30
30
|
<div
|
|
31
|
-
rounded-full flex items-center gap-2px select-none p-2px min-w-68px bg-
|
|
31
|
+
rounded-full flex items-center gap-2px select-none p-2px min-w-68px bg-lowered
|
|
32
32
|
:class="[
|
|
33
33
|
props.size === 'small' ? 'min-h-24px' : 'min-h-28px',
|
|
34
34
|
props.disabled && 'opacity-50 pointer-events-none cursor-not-allowed',
|
|
@@ -47,11 +47,11 @@ function selectTab(value: string) {
|
|
|
47
47
|
:disabled="props.disabled"
|
|
48
48
|
@click="selectTab(option.value)"
|
|
49
49
|
>
|
|
50
|
-
<span relative z-10 :class="modelValue === option.value ? 'text-
|
|
50
|
+
<span relative z-10 :class="modelValue === option.value ? 'text-primary' : 'text-secondary'">{{ option.label }}</span>
|
|
51
51
|
<Motion
|
|
52
52
|
v-if="modelValue === option.value"
|
|
53
53
|
:layout-id="`${uniqueId}-tab-indicator`"
|
|
54
|
-
absolute z-0 inset-0 bg
|
|
54
|
+
absolute z-0 inset-0 bg shadow-tab rounded-inherit size-full
|
|
55
55
|
:transition="{ duration: 0.3, type: 'spring', bounce: 0 }"
|
|
56
56
|
/>
|
|
57
57
|
</button>
|