@finema/finework-layer 0.1.10 → 0.1.12

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.
@@ -1,289 +1,293 @@
1
- <template>
2
- <aside
3
- :class="[
4
- `
5
- flex h-full flex-col overflow-y-auto bg-white
6
- transition-all duration-300
7
- `,
8
- ]"
9
- >
10
- <div
11
- :class="[
12
- 'flex h-[72px] items-center max-sm:pr-5',
13
- {
14
- 'justify-center': isCollapsed,
15
- 'w-[260px] justify-between pl-5': !isCollapsed,
16
- },
17
- ]"
18
- >
19
- <NuxtLink :to="routes.home.to">
20
- <img
21
- v-if="!isCollapsed"
22
- src="/logo-mini.png"
23
- alt="logo_mini_color"
24
- class="h-[27px]"
25
- />
26
- </NuxtLink>
27
-
28
- <Navbar />
29
- </div>
30
- <div
31
- class="bg-primary flex items-center justify-between px-7 py-4 font-bold text-white"
32
- >
33
- {{ isCollapsed ? "" : label }}
34
-
35
- <Icon
36
- v-if="!isCollapsed"
37
- name="fluent:arrow-circle-left-24-regular"
38
- class="size-[24px] cursor-pointer"
39
- @click="$emit('toggle-collapsed')"
40
- />
41
- <Icon
42
- v-else
43
- name="fluent:arrow-circle-right-24-regular"
44
- class="size-[24px] cursor-pointer"
45
- @click="$emit('toggle-collapsed')"
46
- />
47
- </div>
48
-
49
- <div class="flex-1 overflow-y-auto border-r border-gray-200 p-4">
50
- <div v-if="isGroup" class="space-y-4 divide-y-1 divide-[#d9d9d9]">
51
- <div v-for="group in navigationItems" :key="group.label" class="pb-4">
52
- <div class="mt-2 mb-2 text-[10px] font-semibold text-gray-400">
53
- {{ group.label }}
54
- </div>
55
- <NavigationMenu
56
- orientation="vertical"
57
- :items="group.children"
58
- :collapsed="isCollapsed"
59
- :popover="isCollapsed"
60
- :tooltip="isCollapsed"
61
- :ui="{
62
- list: 'space-y-2 ',
63
- label: [
64
- 'text-sm font-bold text-gray-500 py-[12px] px-[10px] rounded-lg',
65
- 'hover:text-primary',
66
- ],
67
- link: [
68
- 'cursor-pointer text-sm font-bold text-gray-500 px-[10px] rounded-lg gap-3',
69
- 'hover:text-primary',
70
- 'data-active:before:bg-white data-active:before:rounded-lg data-active:text-primary font-semibold',
71
- ],
72
- linkLeadingIcon:
73
- 'group-data-[state=open]:text-current text-current size-[24px] group-hover:text-primary',
74
- childList:
75
- 'border-none ms-0 pl-8 bg-gray-100 mt-2 py-1 rounded-lg',
76
- childLink: 'ps-0',
77
- childItem: 'ps-0',
78
- }"
79
- class="w-full justify-center"
80
- />
81
- </div>
82
- </div>
83
- <NavigationMenu
84
- v-else
85
- orientation="vertical"
86
- :items="navigationItems"
87
- :collapsed="isCollapsed"
88
- :popover="isCollapsed"
89
- :tooltip="isCollapsed"
90
- :ui="{
91
- list: 'space-y-2 ',
92
- label: [
93
- 'text-sm font-bold text-gray-500 py-[12px] px-[10px] rounded-lg',
94
- 'hover:text-primary',
95
- ],
96
- link: [
97
- 'cursor-pointer text-sm font-bold text-gray-500 px-[10px] rounded-lg gap-3',
98
- 'hover:text-primary',
99
- 'data-active:before:bg-white data-active:before:rounded-lg data-active:text-primary font-semibold',
100
- ],
101
- linkLeadingIcon:
102
- 'group-data-[state=open]:text-current text-current size-[24px] group-hover:text-primary ',
103
- childList: 'border-none ms-0 pl-8 bg-gray-100 mt-2 py-1 rounded-lg',
104
- childLink: 'ps-0',
105
- childItem: 'ps-0',
106
- }"
107
- class="w-full justify-center"
108
- />
109
- </div>
110
- <div v-if="isMobile" class="border-t border-gray-100 p-3">
111
- <div class="flex items-center justify-between gap-2">
112
- <div class="flex min-w-0 flex-1 items-center gap-3">
113
- <Avatar
114
- class="border-muted size-[32px] flex-shrink-0 border text-lg"
115
- icon="ri:user-line"
116
- :src="auth.me.value?.avatar_url || ''"
117
- />
118
- <div class="flex min-w-0 flex-1 flex-col">
119
- <p class="truncate text-sm font-bold">
120
- {{ auth.me.value?.display_name || auth.me.value?.full_name }}
121
- </p>
122
- <p class="text-muted truncate text-xs">
123
- {{ auth.me.value?.email || "" }}
124
- </p>
125
- </div>
126
- </div>
127
- <DropdownMenu
128
- arrow
129
- size="xl"
130
- :items="userMenuItems"
131
- :ui="{
132
- content: 'w-48',
133
- }"
134
- >
135
- <Button
136
- icon="ph:dots-three-outline-vertical-bold"
137
- variant="ghost"
138
- color="neutral"
139
- size="xs"
140
- />
141
- </DropdownMenu>
142
- </div>
143
- </div>
144
- </aside>
145
- </template>
146
-
147
- <script lang="ts" setup>
148
- import type { NavigationMenuItem } from '@nuxt/ui'
149
- import { computed } from 'vue'
150
- import { useRoute } from 'vue-router'
151
- import type { Permission, UserModule } from '#imports'
152
- import Navbar from '../Apps.vue'
153
-
154
- defineEmits<{
155
- 'toggle-collapsed': []
156
- }>()
157
-
158
- const props = defineProps<{
159
- label: string
160
- isCollapsed: boolean
161
- isMobile?: boolean
162
- items: NavigationMenuItem[]
163
- isGroup?: boolean
164
- }>()
165
-
166
- const route = useRoute()
167
- const auth = useAuth()
168
- const userMenuItems = [
169
- {
170
- label: 'View profile',
171
- icon: 'i-lucide-user',
172
- to: routes.account.profile.to,
173
- },
174
-
175
- {
176
- label: routes.logout.label,
177
- icon: 'i-lucide-log-out',
178
- to: routes.logout.to,
179
- external: true,
180
- },
181
- ]
182
-
183
- const mappedItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
184
- return items.map((item) => {
185
- let isAnyChildActive = false
186
- const mappedChildren = item.children?.map((child) => {
187
- const isChildCurrentlyActive = route.path === child.to
188
-
189
- if (isChildCurrentlyActive) {
190
- isAnyChildActive = true
191
- }
192
-
193
- return {
194
- active: isChildCurrentlyActive,
195
- class:
196
- 'hover:bg-transparent hover:text-gray-700 hover:font-bold py-2 data-active:before:bg-transparent data-active:text-gray-700 data-active:font-bold',
197
- icon: '',
198
- ...child,
199
- }
200
- })
201
-
202
- const selfIsActive = item.to
203
- ? route.path.startsWith(String(item.to))
204
- : false
205
-
206
- const itemIsActive = selfIsActive || isAnyChildActive // A root item is active if its own link matches OR if any child is active
207
-
208
- const itemDefaultOpen = item.children ? isAnyChildActive : false
209
-
210
- return {
211
- ...item,
212
- active: itemIsActive,
213
- class: itemIsActive
214
- ? 'before:bg-primary before:rounded-lg text-white'
215
- : '',
216
- defaultOpen: itemDefaultOpen || selfIsActive,
217
- open: itemDefaultOpen || selfIsActive,
218
- children: mappedChildren,
219
- to: mappedChildren ? undefined : item.to,
220
- }
221
- })
222
- }
223
-
224
- // filter items base on auth.hasPermission and nested children , permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER']
225
- const filterItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
226
- return items
227
- .filter((item) => {
228
- if (item.permissions && Array.isArray(item.permissions)) {
229
- // permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER'] => module: 'pmo', permissions: ['USER', 'ADMIN', 'SUPER']
230
- const permissionStrings = item.permissions as string[]
231
-
232
- // Extract module from first permission (all should have same module)
233
- const firstPermission = permissionStrings[0]
234
-
235
- if (!firstPermission) {
236
- return true
237
- }
238
-
239
- const [moduleStr] = firstPermission.split(':')
240
-
241
- // Extract all permission levels
242
- const permissionLevels = permissionStrings.map(
243
- (p) => p.split(':')[1],
244
- ) as Permission[]
245
-
246
- return auth.hasPermission(moduleStr as UserModule, ...permissionLevels)
247
- }
248
-
249
- // Recursively filter children
250
- if (item.children) {
251
- const filteredChildren = filterItems(
252
- item.children as NavigationMenuItem[],
253
- )
254
-
255
- // Only include parent if it has visible children or no permission requirement
256
- return filteredChildren.length > 0
257
- }
258
-
259
- return true
260
- })
261
- .map((item) => {
262
- // Apply filtering to children
263
- if (item.children) {
264
- return {
265
- ...item,
266
- children: filterItems(item.children as NavigationMenuItem[]),
267
- }
268
- }
269
-
270
- return item
271
- })
272
- }
273
-
274
- const navigationItems = computed<NavigationMenuItem[]>(() => {
275
- // First filter items based on permissions
276
- const filteredItems = filterItems(props.items)
277
-
278
- if (props.isGroup) {
279
- return filteredItems.map((group) => {
280
- return {
281
- ...group,
282
- children: mappedItems(group.children as NavigationMenuItem[]),
283
- }
284
- })
285
- }
286
-
287
- return mappedItems(filteredItems)
288
- })
289
- </script>
1
+ <template>
2
+ <aside
3
+ :class="[
4
+ `
5
+ flex h-full flex-col overflow-y-auto bg-white
6
+ transition-all duration-300
7
+ `,
8
+ ]"
9
+ >
10
+ <div
11
+ :class="[
12
+ 'flex h-[72px] items-center max-sm:pr-5',
13
+ {
14
+ 'justify-center': isCollapsed,
15
+ 'w-[260px] justify-between pl-5': !isCollapsed,
16
+ },
17
+ ]"
18
+ >
19
+ <NuxtLink :to="routes.home.to">
20
+ <img
21
+ v-if="!isCollapsed"
22
+ src="/logo-mini.png"
23
+ alt="logo_mini_color"
24
+ class="h-[27px]"
25
+ />
26
+ </NuxtLink>
27
+
28
+ <Navbar />
29
+ </div>
30
+ <div
31
+ class="bg-primary flex items-center justify-between px-7 py-4 font-bold text-white"
32
+ >
33
+ {{ isCollapsed ? "" : label }}
34
+
35
+ <Icon
36
+ v-if="!isCollapsed"
37
+ name="fluent:arrow-circle-left-24-regular"
38
+ class="size-[24px] cursor-pointer"
39
+ @click="$emit('toggle-collapsed')"
40
+ />
41
+ <Icon
42
+ v-else
43
+ name="fluent:arrow-circle-right-24-regular"
44
+ class="size-[24px] cursor-pointer"
45
+ @click="$emit('toggle-collapsed')"
46
+ />
47
+ </div>
48
+
49
+ <div class="flex-1 overflow-y-auto border-r border-gray-200 p-4">
50
+ <div v-if="isGroup" class="space-y-4 divide-y-1 divide-[#d9d9d9]">
51
+ <div v-for="group in navigationItems" :key="group.label" class="pb-4">
52
+ <div class="mt-2 mb-2 text-[10px] font-semibold text-gray-400">
53
+ {{ group.label }}
54
+ </div>
55
+ <NavigationMenu
56
+ orientation="vertical"
57
+ :items="group.children"
58
+ :collapsed="isCollapsed"
59
+ :popover="isCollapsed"
60
+ :tooltip="isCollapsed"
61
+ :ui="{
62
+ list: 'space-y-2 ',
63
+ label: [
64
+ 'text-sm font-bold text-gray-500 py-[12px] px-[10px] rounded-lg',
65
+ 'hover:text-primary',
66
+ ],
67
+ link: [
68
+ 'cursor-pointer text-sm font-bold text-gray-500 px-[10px] rounded-lg gap-3',
69
+ 'hover:text-primary',
70
+ 'data-active:before:bg-white data-active:before:rounded-lg data-active:text-primary font-semibold',
71
+ ],
72
+ linkLabel: '!text-[#344054]',
73
+ linkLeadingIcon:
74
+ 'group-data-[state=open]:text-current text-current size-[24px] group-hover:text-primary',
75
+ childList: 'border-none ms-0 bg-white mt-2 rounded-lg',
76
+ childLink: 'ps-0',
77
+ childItem: 'ps-0',
78
+ }"
79
+ class="w-full justify-center"
80
+ />
81
+ </div>
82
+ </div>
83
+ <NavigationMenu
84
+ v-else
85
+ orientation="vertical"
86
+ :items="navigationItems"
87
+ :collapsed="isCollapsed"
88
+ :popover="isCollapsed"
89
+ :tooltip="isCollapsed"
90
+ :ui="{
91
+ list: 'space-y-2 ',
92
+ label: [
93
+ 'text-sm font-bold text-gray-500 py-[12px] px-[10px] rounded-lg',
94
+ 'hover:text-primary ',
95
+ ],
96
+ link: [
97
+ 'cursor-pointer text-sm font-bold text-gray-500 px-[10px] rounded-lg gap-3',
98
+ 'hover:text-primary',
99
+ 'data-active:before:bg-[#F9FAFB] data-active:before:rounded-lg data-active:text-primary font-semibold',
100
+ ],
101
+ linkLabel: '!text-[#344054]',
102
+ linkLeadingIcon:
103
+ 'group-data-[state=open]:text-current text-current size-[24px] group-hover:text-primary ',
104
+ childList: 'border-none ms-0 bg-white mt-2 rounded-lg',
105
+ childLink: 'ps-0',
106
+ childItem: 'ps-0',
107
+ }"
108
+ class="w-full justify-center"
109
+ />
110
+ </div>
111
+ <div v-if="isMobile" class="border-t border-gray-100 p-3">
112
+ <div class="flex items-center justify-between gap-2">
113
+ <div class="flex min-w-0 flex-1 items-center gap-3">
114
+ <Avatar
115
+ class="border-muted size-[32px] flex-shrink-0 border text-lg"
116
+ icon="ri:user-line"
117
+ :src="auth.me.value?.avatar_url || ''"
118
+ />
119
+ <div class="flex min-w-0 flex-1 flex-col">
120
+ <p class="truncate text-sm font-bold">
121
+ {{ auth.me.value?.display_name || auth.me.value?.full_name }}
122
+ </p>
123
+ <p class="text-muted truncate text-xs">
124
+ {{ auth.me.value?.email || "" }}
125
+ </p>
126
+ </div>
127
+ </div>
128
+ <DropdownMenu
129
+ arrow
130
+ size="xl"
131
+ :items="userMenuItems"
132
+ :ui="{
133
+ content: 'w-48',
134
+ }"
135
+ >
136
+ <Button
137
+ icon="ph:dots-three-outline-vertical-bold"
138
+ variant="ghost"
139
+ color="neutral"
140
+ size="xs"
141
+ />
142
+ </DropdownMenu>
143
+ </div>
144
+ </div>
145
+ </aside>
146
+ </template>
147
+
148
+ <script lang="ts" setup>
149
+ import type { NavigationMenuItem } from '@nuxt/ui'
150
+ import { computed } from 'vue'
151
+ import { useRoute } from 'vue-router'
152
+ import type { Permission, UserModule } from '#imports'
153
+ import Navbar from '../Apps.vue'
154
+
155
+ defineEmits<{
156
+ 'toggle-collapsed': []
157
+ }>()
158
+
159
+ const props = defineProps<{
160
+ label: string
161
+ isCollapsed: boolean
162
+ isMobile?: boolean
163
+ items: NavigationMenuItem[]
164
+ isGroup?: boolean
165
+ }>()
166
+
167
+ const route = useRoute()
168
+ const auth = useAuth()
169
+ const userMenuItems = [
170
+ {
171
+ label: 'View profile',
172
+ icon: 'i-lucide-user',
173
+ to: routes.account.profile.to,
174
+ },
175
+
176
+ {
177
+ label: routes.logout.label,
178
+ icon: 'i-lucide-log-out',
179
+ to: routes.logout.to,
180
+ external: true,
181
+ },
182
+ ]
183
+
184
+ const normalizePath = (path: string) => path.replace(/\/$/, '')
185
+
186
+ const mappedItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
187
+ return items.map((item) => {
188
+ let isAnyChildActive = false
189
+ const mappedChildren = item.children?.map((child) => {
190
+ const isChildCurrentlyActive
191
+ = normalizePath(route.path) === normalizePath(String(child.to))
192
+
193
+ if (isChildCurrentlyActive) {
194
+ isAnyChildActive = true
195
+ }
196
+
197
+ return {
198
+ active: isChildCurrentlyActive,
199
+ class:
200
+ 'hover:bg-[#F9FAFB] hover:text-gray-700 hover:font-bold py-2 pl-8 data-active:before:bg-[#F9FAFB] data-active:text-primary data-active:font-bold',
201
+ icon: '',
202
+ ...child,
203
+ }
204
+ })
205
+
206
+ const selfIsActive = item.to
207
+ ? normalizePath(route.path) === normalizePath(String(item.to))
208
+ : false
209
+
210
+ const itemIsActive = selfIsActive || isAnyChildActive // A root item is active if its own link matches OR if any child is active
211
+
212
+ const itemDefaultOpen = item.children ? isAnyChildActive : false
213
+
214
+ return {
215
+ ...item,
216
+ active: itemIsActive,
217
+ class: itemIsActive
218
+ ? 'before:bg-[#F9FAFB] before:rounded-lg text-primary'
219
+ : '',
220
+ defaultOpen: itemDefaultOpen || selfIsActive,
221
+ open: itemDefaultOpen || selfIsActive,
222
+ children: mappedChildren,
223
+ to: mappedChildren ? undefined : item.to,
224
+ }
225
+ })
226
+ }
227
+
228
+ // filter items base on auth.hasPermission and nested children , permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER']
229
+ const filterItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
230
+ return items
231
+ .filter((item) => {
232
+ if (item.permissions && Array.isArray(item.permissions)) {
233
+ // permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER'] => module: 'pmo', permissions: ['USER', 'ADMIN', 'SUPER']
234
+ const permissionStrings = item.permissions as string[]
235
+
236
+ // Extract module from first permission (all should have same module)
237
+ const firstPermission = permissionStrings[0]
238
+
239
+ if (!firstPermission) {
240
+ return true
241
+ }
242
+
243
+ const [moduleStr] = firstPermission.split(':')
244
+
245
+ // Extract all permission levels
246
+ const permissionLevels = permissionStrings.map(
247
+ (p) => p.split(':')[1],
248
+ ) as Permission[]
249
+
250
+ return auth.hasPermission(moduleStr as UserModule, ...permissionLevels)
251
+ }
252
+
253
+ // Recursively filter children
254
+ if (item.children) {
255
+ const filteredChildren = filterItems(
256
+ item.children as NavigationMenuItem[],
257
+ )
258
+
259
+ // Only include parent if it has visible children or no permission requirement
260
+ return filteredChildren.length > 0
261
+ }
262
+
263
+ return true
264
+ })
265
+ .map((item) => {
266
+ // Apply filtering to children
267
+ if (item.children) {
268
+ return {
269
+ ...item,
270
+ children: filterItems(item.children as NavigationMenuItem[]),
271
+ }
272
+ }
273
+
274
+ return item
275
+ })
276
+ }
277
+
278
+ const navigationItems = computed<NavigationMenuItem[]>(() => {
279
+ // First filter items based on permissions
280
+ const filteredItems = filterItems(props.items)
281
+
282
+ if (props.isGroup) {
283
+ return filteredItems.map((group) => {
284
+ return {
285
+ ...group,
286
+ children: mappedItems(group.children as NavigationMenuItem[]),
287
+ }
288
+ })
289
+ }
290
+
291
+ return mappedItems(filteredItems)
292
+ })
293
+ </script>