@meistrari/tela-build 1.44.0 → 1.46.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.
|
@@ -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>
|
package/package.json
CHANGED
package/plugins/test-id.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { DirectiveBinding } from 'vue'
|
|
2
1
|
import { defineNuxtPlugin } from 'nuxt/app'
|
|
3
2
|
|
|
4
3
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
@@ -6,20 +5,20 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
6
5
|
return
|
|
7
6
|
}
|
|
8
7
|
|
|
9
|
-
function updateTestId(el: HTMLElement,
|
|
10
|
-
if (
|
|
8
|
+
function updateTestId(el: HTMLElement, value: unknown) {
|
|
9
|
+
if (value === undefined || value === null || value === '') {
|
|
11
10
|
el.removeAttribute('data-testid')
|
|
12
11
|
return
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
el.setAttribute('data-testid', String(
|
|
14
|
+
el.setAttribute('data-testid', String(value))
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
nuxtApp.vueApp.directive('test', {
|
|
19
|
-
created: updateTestId,
|
|
18
|
+
created: (el, binding) => updateTestId(el, binding.value),
|
|
20
19
|
updated(el, binding) {
|
|
21
20
|
if (binding.value !== binding.oldValue) {
|
|
22
|
-
updateTestId(el, binding)
|
|
21
|
+
updateTestId(el, binding.value)
|
|
23
22
|
}
|
|
24
23
|
},
|
|
25
24
|
})
|