@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.44.0",
3
+ "version": "1.46.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",
@@ -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, binding: DirectiveBinding<unknown>) {
10
- if (binding.value === undefined || binding.value === null || binding.value === '') {
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(binding.value))
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
  })