@meistrari/tela-build 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.
@@ -22,7 +22,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
22
22
  <MenubarItem
23
23
  v-bind="forwarded"
24
24
  :class="cn(
25
- 'relative flex cursor-pointer select-none items-center rounded-xl px-3 py-1.5 text-body-14-medium font-460 outline-none focus:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-40',
25
+ 'relative flex cursor-pointer select-none items-center rounded-xl px-3 py-1.5 text-body-14-medium font-460 outline-none focus:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-40',
26
26
  inset && 'pl-8',
27
27
  props.class,
28
28
  )"
@@ -17,7 +17,7 @@ const forwardedProps = useForwardProps(delegatedProps)
17
17
  v-bind="forwardedProps"
18
18
  :class="
19
19
  cn(
20
- 'flex items-center gap-2 cursor-pointer select-none px-2 py-1.5 text-sm font-medium outline-none rounded-lg hover:bg-gray-200 data-[state=open]:bg-gray-200',
20
+ 'flex items-center gap-2 cursor-pointer select-none px-2 py-1.5 text-sm font-medium outline-none rounded-lg hover:bg-lowered data-[state=open]:bg-lowered',
21
21
  props.class,
22
22
  )
23
23
  "
@@ -0,0 +1,10 @@
1
+ <template>
2
+ <div
3
+ flex flex-col h-full overflow-y-auto no-scrollbar
4
+ class="[mask-image:linear-gradient(to_bottom,transparent,black_20px,black_calc(100%_-_20px),transparent)]"
5
+ >
6
+ <nav flex flex-col items-center gap-14px h-full px-10px pt-12px mb-48px>
7
+ <slot />
8
+ </nav>
9
+ </div>
10
+ </template>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <footer flex flex-col items-center justify-center gap-22px pb-24px>
3
+ <slot />
4
+ </footer>
5
+ </template>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <header sticky top-0 z-10 flex flex-col items-center justify-center gap-24px pt-24px pb-12px>
3
+ <slot />
4
+ </header>
5
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import { NuxtLink } from '#components'
3
+
4
+ const props = defineProps<{
5
+ icon: string
6
+ label: string
7
+ to?: string
8
+ onClick?: () => void
9
+ isActive: boolean
10
+ }>()
11
+
12
+ const iconName = computed(() => props.isActive ? `${props.icon}-fill` : props.icon)
13
+ </script>
14
+
15
+ <template>
16
+ <component
17
+ :is="to ? NuxtLink : 'button'"
18
+ :to="to"
19
+ :type="!to ? 'button' : undefined"
20
+ class="group"
21
+ flex="~ col" items-center justify-center gap-2px outline-none
22
+ :data-active="isActive"
23
+ v-bind="!to && onClick ? { onClick } : {}"
24
+ >
25
+ <div relative size-40px flex items-center justify-center rounded-10px>
26
+ <TelaIcon
27
+ :name="iconName"
28
+ size="20px"
29
+ relative z-1
30
+ :color="isActive ? 'icon' : 'icon-tertiary duration-150 ease-out group-hover:icon group-focus-within:icon'"
31
+ />
32
+ <div
33
+ :class="cn(
34
+ 'absolute inset-0 size-full rounded-[14px] z-0 border-[0.5px] border-transparent',
35
+ isActive ? 'bg-neutral-200 group-focus-within:border-strong' : 'bg scale-10 opacity-0 duration-150 ease-out origin-center group-hover:border-strong group-hover:scale-100 group-hover:opacity-100 group-focus-within:border-strong group-focus-within:scale-100 group-focus-within:opacity-100',
36
+ )"
37
+ />
38
+ </div>
39
+ <p
40
+ :class="cn(
41
+ 'text-[11px] leading-[12px] -tracking-0.2px',
42
+ isActive ? 'text-primary font-550' : 'font-460 text-tertiary duration-150 ease-out group-hover:text-primary group-focus-within:text-primary',
43
+ )"
44
+ >
45
+ {{ label }}
46
+ </p>
47
+ </component>
48
+ </template>
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import { NuxtLink } from '#components'
3
+
4
+ defineProps<{
5
+ src?: string
6
+ alt: string
7
+ to?: string
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <component
13
+ :is="to ? NuxtLink : 'div'"
14
+ :to="to"
15
+ class="rounded-12px overflow-hidden outline-none focus-within:ring-0.5px focus-within:ring-border-strong"
16
+ >
17
+ <img v-if="src" :src="src" :alt="alt" w-40px h-40px object-cover>
18
+ <TelaInitials v-else-if="alt" size="md" :word="alt" />
19
+ <div v-else w-40px h-40px bg-black />
20
+ </component>
21
+ </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ image?: string
4
+ name: string
5
+ email: string
6
+ actions: {
7
+ label: string
8
+ icon: string
9
+ color: 'positive' | 'caution' | 'negative'
10
+ onClick: () => void
11
+ }[]
12
+ }>()
13
+ </script>
14
+
15
+ <template>
16
+ <TelaDropdownMenuRoot>
17
+ <TelaDropdownMenuTrigger as-child>
18
+ <button
19
+ :class="cn('group', image ? 'ring-transparent focus-within:ring-border-strong' : 'ring-border-strong focus-within:ring-border-accent')"
20
+ rounded-full overflow-hidden ring-0.5px
21
+ >
22
+ <img v-if="image" :src="image" :alt="name" w-32px h-32px object-cover>
23
+ <div v-else w-32px h-32px bg />
24
+ </button>
25
+ </TelaDropdownMenuTrigger>
26
+ <TelaDropdownMenuContent align="start" side="top" class="min-w-200px!">
27
+ <div px-12px py-8px>
28
+ <h5 heading-h5-semibold text-primary mb-2px>
29
+ {{ name }}
30
+ </h5>
31
+ <p body-12-regular text-secondary>
32
+ {{ email }}
33
+ </p>
34
+ </div>
35
+ <TelaDropdownMenuSeparator />
36
+ <TelaDropdownMenuItem
37
+ v-for="action in actions"
38
+ :key="action.label"
39
+ :icon="action.icon"
40
+ :color="action.color"
41
+ @click="action.onClick"
42
+ >
43
+ {{ action.label }}
44
+ </TelaDropdownMenuItem>
45
+ </TelaDropdownMenuContent>
46
+ </TelaDropdownMenuRoot>
47
+ </template>
@@ -0,0 +1,175 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as SidebarStories from './sidebar.stories.ts';
3
+
4
+ <Meta of={SidebarStories} />
5
+
6
+ # TelaSidebar
7
+
8
+ A composable sidebar navigation system built from focused sub-components. Fixed 80px wide and full-height, designed for icon-based navigation with labels.
9
+
10
+ ## Examples
11
+
12
+ ### Full Sidebar
13
+
14
+ ```vue
15
+ <TelaSidebar>
16
+ <TelaSidebarHeader>
17
+ <TelaSidebarLogo src="/tela-logo-black.svg" alt="Tela Logo" />
18
+ </TelaSidebarHeader>
19
+
20
+ <TelaSidebarContent>
21
+ <TelaSidebarItem
22
+ v-for="item in items"
23
+ :key="item.label"
24
+ :icon="item.icon"
25
+ :label="item.label"
26
+ :to="item.path"
27
+ :is-active="isItemActive(item)"
28
+ :on-click="item.onClick"
29
+ />
30
+ </TelaSidebarContent>
31
+
32
+ <TelaSidebarFooter>
33
+ <TelaSidebarItem
34
+ icon="i-ph-bell"
35
+ label="Activity"
36
+ :is-active="route.path === '/activity'"
37
+ />
38
+ <TelaSidebarUser
39
+ :image="user.image"
40
+ :name="user.name"
41
+ :email="user.email"
42
+ :actions="[
43
+ { label: 'Logout', icon: 'i-ph-sign-out', color: 'negative', onClick: handleLogout },
44
+ ]"
45
+ />
46
+ </TelaSidebarFooter>
47
+ </TelaSidebar>
48
+ ```
49
+
50
+ <Canvas of={SidebarStories.Default} />
51
+
52
+ ### Activity Active
53
+
54
+ <Canvas of={SidebarStories.ActivityActive} />
55
+
56
+ ### No Active Item
57
+
58
+ <Canvas of={SidebarStories.NoActiveItem} />
59
+
60
+ ### Individual Item States
61
+
62
+ <Canvas of={SidebarStories.SingleItem} />
63
+
64
+ ## Components
65
+
66
+ ### `<TelaSidebar>`
67
+
68
+ Root wrapper. Renders as an `<aside>` with fixed 80px width, full height, and a right border. Place `TelaSidebarHeader`, `TelaSidebarContent`, and `TelaSidebarFooter` as direct children.
69
+
70
+ ### `<TelaSidebarHeader>`
71
+
72
+ Top section of the sidebar. Stacks children vertically with 24px gap and vertical padding. Use for logos or workspace switchers.
73
+
74
+ ### `<TelaSidebarLogo>`
75
+
76
+ Displays a logo or fallback inside the header. When `src` is provided, renders an image. When only `alt` is provided, falls back to `TelaInitials`. If neither is set, renders a placeholder div.
77
+
78
+ ```typescript
79
+ interface TelaSidebarLogoProps {
80
+ src?: string // Image source URL or path — when provided, renders an <img>
81
+ alt: string // Accessible alt text — used as the word for TelaInitials fallback
82
+ to?: string // Optional link — wraps the content in a NuxtLink when provided
83
+ }
84
+ ```
85
+
86
+ ### `<TelaSidebarContent>`
87
+
88
+ Middle section that grows to fill available space. Scrollable with hidden scrollbar and a fade mask at the top and bottom edges for a smooth overflow effect. Place nav items directly as children — they stack vertically with 14px gap.
89
+
90
+ ### `<TelaSidebarItem>`
91
+
92
+ An individual navigation item with an icon and label. Renders as a `NuxtLink` when `to` is provided, otherwise as a `<button>`.
93
+
94
+ ```typescript
95
+ interface TelaSidebarItemProps {
96
+ icon: string // Iconify class (e.g. "i-ph-house")
97
+ label: string // Text label shown below the icon
98
+ isActive: boolean // Highlights the item as the current route
99
+ to?: string // Route path — renders as a link
100
+ onClick?: () => void // Click handler when not using `to`
101
+ }
102
+ ```
103
+
104
+ <ArgTypes of={SidebarStories} />
105
+
106
+ ### `<TelaSidebarFooter>`
107
+
108
+ Bottom section of the sidebar. Always contains `TelaSidebarUser` for account controls. Optionally add secondary `TelaSidebarItem` actions (e.g. activity, notifications) above it.
109
+
110
+ Default — user only:
111
+
112
+ ```vue
113
+ <TelaSidebarFooter>
114
+ <TelaSidebarUser
115
+ :avatar-url="user.avatarUrl"
116
+ :username="user.name"
117
+ :email="user.email"
118
+ :options="[
119
+ { label: 'Logout', icon: 'i-ph-sign-out', color: 'negative', onClick: handleLogout },
120
+ ]"
121
+ />
122
+ </TelaSidebarFooter>
123
+ ```
124
+
125
+ With secondary actions:
126
+
127
+ ```vue
128
+ <TelaSidebarFooter>
129
+ <TelaSidebarItem
130
+ icon="i-ph-bell"
131
+ label="Activity"
132
+ :is-active="route.path === '/activity'"
133
+ to="/activity"
134
+ />
135
+ <TelaSidebarUser
136
+ :avatar-url="user.avatarUrl"
137
+ :username="user.name"
138
+ :email="user.email"
139
+ :options="[
140
+ { label: 'Logout', icon: 'i-ph-sign-out', color: 'negative', onClick: handleLogout },
141
+ ]"
142
+ />
143
+ </TelaSidebarFooter>
144
+ ```
145
+
146
+ ### `<TelaSidebarUser>`
147
+
148
+ User account button in the footer. Clicking opens a dropdown menu with the user's name, email, and configurable action items.
149
+
150
+ ```typescript
151
+ interface TelaSidebarUserProps {
152
+ image?: string // Optional — falls back to a placeholder if not provided
153
+ name: string
154
+ email: string
155
+ actions: {
156
+ label: string
157
+ icon: string
158
+ color: 'positive' | 'caution' | 'negative'
159
+ onClick: () => void
160
+ }[]
161
+ }
162
+ ```
163
+
164
+ Example:
165
+
166
+ ```vue
167
+ <TelaSidebarUser
168
+ image="https://example.com/avatar.jpg"
169
+ name="Jane Doe"
170
+ email="jane@example.com"
171
+ :actions="[
172
+ { label: 'Logout', icon: 'i-ph-sign-out', color: 'negative', onClick: handleLogout },
173
+ ]"
174
+ />
175
+ ```
@@ -0,0 +1,190 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import TelaSidebar from './sidebar.vue'
3
+ import TelaSidebarHeader from './sidebar-header.vue'
4
+ import TelaSidebarContent from './sidebar-content.vue'
5
+ import TelaSidebarLogo from './sidebar-logo.vue'
6
+ import TelaSidebarItem from './sidebar-item.vue'
7
+ import TelaSidebarFooter from './sidebar-footer.vue'
8
+ import TelaSidebarUser from './sidebar-user.vue'
9
+
10
+ const meta: Meta<typeof TelaSidebar> = {
11
+ title: 'Patterns/Sidebar',
12
+ component: TelaSidebar,
13
+ parameters: {
14
+ layout: 'fullscreen',
15
+ docs: {
16
+ description: {
17
+ component: 'A composable sidebar navigation system built from focused sub-components. Fixed 80px wide and full-height, designed for icon-based navigation with labels. Compose `TelaSidebarHeader`, `TelaSidebarContent`, and `TelaSidebarFooter` inside the root `TelaSidebar` to build the full layout.',
18
+ },
19
+ },
20
+ },
21
+ }
22
+
23
+ export default meta
24
+
25
+ type Story = StoryObj<typeof TelaSidebar>
26
+
27
+ const navItems = [
28
+ { icon: 'i-ph-house', label: 'Home', path: '/' },
29
+ { icon: 'i-ph-graph', label: 'Workflows', path: '/workflows' },
30
+ { icon: 'i-ph-database', label: 'Data', path: '/data' },
31
+ { icon: 'i-ph-gear', label: 'Settings', path: '/settings' },
32
+ ]
33
+
34
+ const userActions = [
35
+ { label: 'Logout', icon: 'i-ph-sign-out', color: 'negative' as const, onClick: () => {} },
36
+ ]
37
+
38
+ const components = {
39
+ TelaSidebar,
40
+ TelaSidebarHeader,
41
+ TelaSidebarLogo,
42
+ TelaSidebarContent,
43
+ TelaSidebarItem,
44
+ TelaSidebarFooter,
45
+ TelaSidebarUser,
46
+ }
47
+
48
+ export const Default: Story = {
49
+ render: () => ({
50
+ components,
51
+ setup() {
52
+ const items = navItems
53
+ const activePath = '/workflows'
54
+ const isItemActive = (item: { path: string }) => activePath === item.path
55
+ return { items, isItemActive, userActions }
56
+ },
57
+ template: `
58
+ <TelaSidebar>
59
+ <TelaSidebarHeader>
60
+ <TelaSidebarLogo src="/tela-logo-black.svg" alt="Tela Logo" />
61
+ </TelaSidebarHeader>
62
+
63
+ <TelaSidebarContent>
64
+ <TelaSidebarItem
65
+ v-for="item in items"
66
+ :key="item.label"
67
+ :icon="item.icon"
68
+ :label="item.label"
69
+ :to="item.path"
70
+ :is-active="isItemActive(item)"
71
+ />
72
+ <TelaSidebarItem
73
+ icon="i-ph-bell"
74
+ label="Activity"
75
+ :is-active="false"
76
+ />
77
+ </TelaSidebarContent>
78
+
79
+ <TelaSidebarFooter>
80
+ <TelaSidebarUser
81
+ name="Username"
82
+ email="user@example.com"
83
+ :actions="userActions"
84
+ />
85
+ </TelaSidebarFooter>
86
+ </TelaSidebar>
87
+ `,
88
+ }),
89
+ }
90
+
91
+ export const NoActiveItem: Story = {
92
+ render: () => ({
93
+ components,
94
+ setup() {
95
+ return { items: navItems, userActions }
96
+ },
97
+ template: `
98
+ <TelaSidebar>
99
+ <TelaSidebarHeader>
100
+ <TelaSidebarLogo src="/tela-logo-black.svg" alt="Tela Logo" />
101
+ </TelaSidebarHeader>
102
+
103
+ <TelaSidebarContent>
104
+ <TelaSidebarItem
105
+ v-for="item in items"
106
+ :key="item.label"
107
+ :icon="item.icon"
108
+ :label="item.label"
109
+ :to="item.path"
110
+ :is-active="false"
111
+ />
112
+ <TelaSidebarItem
113
+ icon="i-ph-bell"
114
+ label="Activity"
115
+ :is-active="false"
116
+ />
117
+ </TelaSidebarContent>
118
+
119
+ <TelaSidebarFooter>
120
+ <TelaSidebarUser
121
+ name="Username"
122
+ email="user@example.com"
123
+ :actions="userActions"
124
+ />
125
+ </TelaSidebarFooter>
126
+ </TelaSidebar>
127
+ `,
128
+ }),
129
+ }
130
+
131
+ export const ActivityActive: Story = {
132
+ render: () => ({
133
+ components,
134
+ setup() {
135
+ return { items: navItems, userActions }
136
+ },
137
+ template: `
138
+ <TelaSidebar>
139
+ <TelaSidebarHeader>
140
+ <TelaSidebarLogo src="/tela-logo-black.svg" alt="Tela Logo" />
141
+ </TelaSidebarHeader>
142
+
143
+ <TelaSidebarContent>
144
+ <TelaSidebarItem
145
+ v-for="item in items"
146
+ :key="item.label"
147
+ :icon="item.icon"
148
+ :label="item.label"
149
+ :to="item.path"
150
+ :is-active="false"
151
+ />
152
+ <TelaSidebarItem
153
+ icon="i-ph-bell"
154
+ label="Activity"
155
+ :is-active="true"
156
+ />
157
+ </TelaSidebarContent>
158
+
159
+ <TelaSidebarFooter>
160
+ <TelaSidebarUser
161
+ name="Username"
162
+ email="user@example.com"
163
+ :actions="userActions"
164
+ />
165
+ </TelaSidebarFooter>
166
+ </TelaSidebar>
167
+ `,
168
+ }),
169
+ }
170
+
171
+ export const SingleItem: Story = {
172
+ parameters: {
173
+ layout: 'centered',
174
+ docs: {
175
+ description: {
176
+ story: 'Individual `TelaSidebarItem` in active and inactive states.',
177
+ },
178
+ },
179
+ },
180
+ render: () => ({
181
+ components: { TelaSidebarItem },
182
+ template: `
183
+ <div style="display: flex; gap: 24px; align-items: center;">
184
+ <TelaSidebarItem icon="i-ph-house" label="Home" :is-active="false" />
185
+ <TelaSidebarItem icon="i-ph-graph" label="Workflows" :is-active="true" />
186
+ <TelaSidebarItem icon="i-ph-bell" label="Activity" :is-active="false" />
187
+ </div>
188
+ `,
189
+ }),
190
+ }
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <aside flex flex-col items-center w-80px h-screen bg border-r-0.5px border>
3
+ <slot />
4
+ </aside>
5
+ </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",
package/unocss.config.ts CHANGED
@@ -1,8 +1,19 @@
1
1
  import transformerDirectives from '@unocss/transformer-directives'
2
2
  import { defineConfig, toEscapedSelector } from 'unocss'
3
+ import type { Extractor } from 'unocss'
3
4
  import { DT } from './utils/design-tokens'
4
5
 
6
+ /* Enables fill variants for Phosphor icons used as a dynamic suffix (e.g. `${icon}-fill`)
7
+ * in components, which UnoCSS can't detect through static scanning */
8
+ const phosphorFillExtractor: Extractor = {
9
+ name: 'phosphor-fill-icons',
10
+ extract: ({ code }) => new Set(
11
+ [...code.matchAll(/['"`](i-ph-[\w-]+)['"`]/g)].map(([, icon]) => `${icon}-fill`),
12
+ ),
13
+ }
14
+
5
15
  export default defineConfig({
16
+ extractors: [phosphorFillExtractor],
6
17
  theme: {
7
18
  ...DT,
8
19
  animation: {