@finema/finework-layer 0.1.16 → 0.1.17

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