@finema/finework-layer 0.2.75 → 0.2.76

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.
Files changed (51) hide show
  1. package/.husky/pre-commit +1 -1
  2. package/.playground/.env.example +5 -0
  3. package/.playground/app/assets/css/main.css +6 -6
  4. package/.playground/app/pages/layout-admin/[id]/index.vue +145 -145
  5. package/.playground/app/pages/layout-admin/test/[id]/index.vue +286 -286
  6. package/.playground/app/pages/layout-admin.vue +285 -285
  7. package/.playground/app/pages/layout-user.vue +284 -0
  8. package/.playground/app/pages/submenu/layout-admin.vue +210 -210
  9. package/.playground/public/admin/clock-in-admin-logo.png +0 -0
  10. package/.playground/public/admin/clock-in-logo.png +0 -0
  11. package/.playground/public/admin/clock-in.png +0 -0
  12. package/.playground/public/admin/pmo-logo.png +0 -0
  13. package/.playground/public/admin/spider-web.png +0 -0
  14. package/.playground/public/admin/super-admin-logo.png +0 -0
  15. package/.playground/public/admin/super-admin.png +0 -0
  16. package/.playground/public/admin/timesheet-admin-logo.png +0 -0
  17. package/.playground/public/admin/timesheet-logo.png +0 -0
  18. package/.playground/public/admin/timesheet.png +0 -0
  19. package/CHANGELOG.md +374 -367
  20. package/app/app.config.ts +144 -144
  21. package/app/app.vue +10 -10
  22. package/app/assets/css/main.css +77 -77
  23. package/app/components/Button/ActionIcon.vue +29 -29
  24. package/app/components/Button/Back.vue +22 -22
  25. package/app/components/Format/Currency.vue +17 -17
  26. package/app/components/Format/Date.vue +24 -24
  27. package/app/components/Format/DateTime.vue +24 -24
  28. package/app/components/Format/Number.vue +17 -17
  29. package/app/components/Format/Percent.vue +38 -38
  30. package/app/components/Format/TimeFromNow.vue +38 -38
  31. package/app/components/InfoItemList.vue +196 -196
  32. package/app/components/Layout/Admin/Sidebar.vue +329 -329
  33. package/app/components/Layout/Admin/index.vue +224 -270
  34. package/app/components/Layout/Apps.vue +45 -45
  35. package/app/components/Layout/User/index.vue +102 -145
  36. package/app/components/Notifications/index.vue +147 -0
  37. package/app/components/StatusBox.vue +56 -56
  38. package/app/composables/useAuth.ts +207 -207
  39. package/app/composables/useNotification.ts +64 -0
  40. package/app/composables/useRequestOptions.ts +86 -86
  41. package/app/constants/routes.ts +86 -86
  42. package/app/error.vue +218 -218
  43. package/app/middleware/auth.ts +45 -45
  44. package/app/middleware/common.ts +12 -12
  45. package/app/middleware/guest.ts +7 -7
  46. package/app/middleware/permissions.ts +29 -29
  47. package/bun.lock +2758 -2758
  48. package/index.d.ts +16 -16
  49. package/nuxt.config.ts +41 -41
  50. package/package.json +38 -38
  51. package/app/components/NotificationList.vue +0 -65
@@ -1,270 +1,224 @@
1
- <template>
2
- <div class="relative flex min-h-screen flex-1">
3
- <div
4
- :class="[
5
- `
6
- fixed inset-0 z-[102] hidden w-auto
7
- lg:block
8
- `,
9
- {
10
- 'max-w-[88px]': isCollapsed,
11
- 'max-w-[260px]': !isCollapsed,
12
- },
13
- ]"
14
- >
15
- <Sidebar
16
- :label="label"
17
- :is-collapsed="isCollapsed"
18
- :items="sidebarItems"
19
- :is-group="isSidebarGroup"
20
- @toggle-collapsed="isCollapsed = !isCollapsed"
21
- />
22
- </div>
23
- <div
24
- :class="[
25
- 'w-full bg-gray-50',
26
- {
27
- 'lg:pl-[88px]': isCollapsed,
28
- 'lg:pl-[260px]': !isCollapsed,
29
- },
30
- ]"
31
- >
32
- <nav
33
- class="fixed top-0 left-0 z-[101] flex min-h-[64px] w-screen items-center justify-between gap-4 bg-white px-5 lg:min-h-[72px] lg:justify-end"
34
- style="box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.04)"
35
- >
36
- <div class="flex items-center gap-4">
37
- <Slideover
38
- v-model:open="isShowSidebarMobile"
39
- :ui="{
40
- content: 'w-[80%] max-w-[260px] lg:hidden',
41
- overlay: 'lg:hidden',
42
- }"
43
- side="left"
44
- >
45
- <svg
46
- class="cursor-pointer lg:hidden"
47
- width="19"
48
- height="18"
49
- viewBox="0 0 19 18"
50
- fill="none"
51
- xmlns="http://www.w3.org/2000/svg"
52
- transform="scale(-1, 1)"
53
- @click="isShowSidebarMobile = true"
54
- >
55
- <path
56
- d="M10 18L10 16L19 16L19 18L10 18ZM6 2L6 -2.62268e-07L19 -8.30516e-07L19 2L6 2ZM-3.49691e-07 10L-4.37114e-07 8L19 8L19 10L-3.49691e-07 10Z"
57
- fill="#4B5563"
58
- />
59
- </svg>
60
-
61
- <template #content>
62
- <Sidebar
63
- :label="label"
64
- :is-collapsed="false"
65
- :is-mobile="true"
66
- :items="sidebarItems"
67
- :is-group="isSidebarGroup"
68
- @toggle-collapsed="isShowSidebarMobile = false"
69
- />
70
- </template>
71
- </Slideover>
72
- <NuxtLink :to="routes.home.to">
73
- <img
74
- src="/logo-mini.png"
75
- alt="logo_mini_color"
76
- class="w-[126px] min-w-[126px] lg:hidden lg:w-[172px] lg:min-w-[172px]"
77
- />
78
- </NuxtLink>
79
- <Apps class="lg:hidden" />
80
- </div>
81
-
82
- <div class="flex items-center justify-center gap-4">
83
- <Slideover
84
- :ui="{
85
- content: 'mt-18',
86
- header: 'border-0',
87
- }"
88
- :modal="false"
89
- title="Notifications"
90
- >
91
- <Chip
92
- inset
93
- color="success"
94
- >
95
- <Icon
96
- name="ph:bell"
97
- class="size-6 cursor-pointer"
98
- />
99
- </Chip>
100
-
101
- <template #body>
102
- <NotificationList :items="notifications" />
103
- </template>
104
- </Slideover>
105
- <DropdownMenu
106
- size="xl"
107
- :items="userMenuItems"
108
- :content="{
109
- align: 'end',
110
- side: 'bottom',
111
- }"
112
- :ui="{
113
- content: 'w-48',
114
- }"
115
- >
116
- <div class="relative flex cursor-pointer items-center gap-2">
117
- <div class="hidden flex-col justify-end text-right lg:flex">
118
- <p class="font-bold">
119
- {{ auth.me.value?.display_name || auth.me.value?.full_name }}
120
- </p>
121
- <p class="text-muted text-sm">
122
- {{ auth.me.value?.email || "" }}
123
- </p>
124
- </div>
125
- <Avatar
126
- class="border-muted size-[40px] border text-2xl"
127
- icon="ri:user-line"
128
- :src="auth.me.value?.avatar_url || ''"
129
- />
130
- <Icon
131
- name="i-ph:caret-down-light"
132
- class="size-5"
133
- />
134
- </div>
135
- </DropdownMenu>
136
- </div>
137
- </nav>
138
- <div class="w-full bg-gray-50 pt-[64px] lg:pt-[72px]">
139
- <main
140
- :class="[
141
- 'mx-auto min-h-full flex-1 px-6 py-10 lg:px-8',
142
- fullScreen ? 'w-full 2xl:max-w-[90%]' : 'max-w-7xl 2xl:max-w-[70%]',
143
- ]"
144
- >
145
- <Breadcrumb
146
- v-if="
147
- !app.pageMeta.isHideBreadcrumbs && breadcrumbsItems.length > 1
148
- "
149
- :items="breadcrumbsItems"
150
- class="mb-5"
151
- :ui="{
152
- item: 'max-w-2/3',
153
- list: 'w-full',
154
- }"
155
- />
156
- <div
157
- v-if="app.pageMeta.title"
158
- class="mb-4 flex flex-col justify-between gap-1 md:mb-6 md:gap-4 lg:flex-row lg:items-start"
159
- >
160
- <div class="flex flex-1 flex-col">
161
- <h1
162
- class="text-3xl font-bold wrap-break-word lg:max-w-2/3"
163
- :title="app.pageMeta.title"
164
- >
165
- {{ app.pageMeta.title }}
166
- <span id="page-title-extra" />
167
- </h1>
168
-
169
- <div id="page-subtitle" />
170
- <p
171
- v-if="app.pageMeta.sub_title"
172
- class="text-[#475467]"
173
- >
174
- {{ app.pageMeta.sub_title }}
175
- </p>
176
- </div>
177
- <div id="page-header" />
178
- </div>
179
- <slot />
180
- </main>
181
- </div>
182
- </div>
183
- </div>
184
- </template>
185
-
186
- <script lang="ts" setup>
187
- import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
188
- import { computed } from 'vue'
189
- import Sidebar from './Sidebar.vue'
190
- import Apps from '../Apps.vue'
191
-
192
- const props = withDefaults(
193
- defineProps<{
194
- label: string
195
- sidebarItems: NavigationMenuItem[]
196
- isSidebarGroup?: boolean
197
- fullScreen?: boolean
198
- rootAppPath?: string
199
- }>(),
200
- {
201
- fullScreen: true,
202
- },
203
- )
204
-
205
- const app = useApp()
206
- const isShowSidebarMobile = ref(false)
207
- const auth = useAuth()
208
- // Cookie to store user preference for desktop
209
- const isCollapsed = useCookie<boolean>('app.admin.sidebar.isCollapsed', {
210
- default: () => false,
211
- path: '/',
212
- })
213
-
214
- const notifications = [
215
- {
216
- id: 1,
217
- userName: 'Phoenix Baker',
218
- avatarSrc: '/mock/user-1.png',
219
- date: '2025-01-01T08:00:00Z',
220
- type: 'file',
221
- description: 'Added a file to Marketing site redesign',
222
- fileName: 'Tech requirements.pdf',
223
- fileSize: '720 KB',
224
- fileIcon: 'ph:file-pdf',
225
- isRead: true,
226
- },
227
- {
228
- id: 2,
229
- userName: 'Lana Steiner',
230
- avatarSrc: '/mock/user-2.png',
231
- date: '2025-01-01T08:00:00Z',
232
- type: 'invite',
233
- description: 'Invited Alisa Hester to the team',
234
- isRead: false,
235
- },
236
- ]
237
-
238
- const userMenuItems: DropdownMenuItem[] = [
239
- {
240
- label: 'View profile',
241
- icon: 'i-lucide-user',
242
- to: routes.account.profile.to,
243
- },
244
-
245
- {
246
- label: routes.logout.label,
247
- icon: 'i-lucide-log-out',
248
- to: routes.logout.to,
249
- external: true,
250
- },
251
- ]
252
-
253
- const breadcrumbsItems = computed(() => {
254
- const list = app.pageMeta.breadcrumbs || []
255
- if (!list.length) return []
256
- const extra = list.map((item: any) => ({
257
- ...item,
258
- icon: '',
259
- }))
260
-
261
- return [
262
- {
263
- label: '',
264
- icon: 'ph:house-line',
265
- to: props.rootAppPath || '/',
266
- },
267
- ...extra,
268
- ]
269
- })
270
- </script>
1
+ <template>
2
+ <div class="relative flex min-h-screen flex-1">
3
+ <div
4
+ :class="[
5
+ `
6
+ fixed inset-0 z-[102] -mt-0.5 hidden w-auto
7
+ lg:block
8
+ `,
9
+ {
10
+ 'max-w-[88px]': isCollapsed,
11
+ 'max-w-[260px]': !isCollapsed,
12
+ },
13
+ ]"
14
+ >
15
+ <Sidebar
16
+ :label="label"
17
+ :is-collapsed="isCollapsed"
18
+ :items="sidebarItems"
19
+ :is-group="isSidebarGroup"
20
+ @toggle-collapsed="isCollapsed = !isCollapsed"
21
+ />
22
+ </div>
23
+ <div
24
+ :class="[
25
+ 'w-full bg-gray-50',
26
+ {
27
+ 'lg:pl-[88px]': isCollapsed,
28
+ 'lg:pl-[260px]': !isCollapsed,
29
+ },
30
+ ]"
31
+ >
32
+ <nav
33
+ class="fixed top-0 left-0 z-[101] flex min-h-[64px] w-screen items-center justify-between gap-4 border-b border-gray-300 bg-white px-5 lg:min-h-[72px] lg:justify-end"
34
+ >
35
+ <div class="flex items-center gap-4">
36
+ <Slideover
37
+ v-model:open="isShowSidebarMobile"
38
+ :ui="{
39
+ content: 'w-[80%] max-w-[260px] lg:hidden',
40
+ overlay: 'lg:hidden',
41
+ }"
42
+ side="left"
43
+ >
44
+ <svg
45
+ class="cursor-pointer lg:hidden"
46
+ width="19"
47
+ height="18"
48
+ viewBox="0 0 19 18"
49
+ fill="none"
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ transform="scale(-1, 1)"
52
+ @click="isShowSidebarMobile = true"
53
+ >
54
+ <path
55
+ d="M10 18L10 16L19 16L19 18L10 18ZM6 2L6 -2.62268e-07L19 -8.30516e-07L19 2L6 2ZM-3.49691e-07 10L-4.37114e-07 8L19 8L19 10L-3.49691e-07 10Z"
56
+ fill="#4B5563"
57
+ />
58
+ </svg>
59
+
60
+ <template #content>
61
+ <Sidebar
62
+ :label="label"
63
+ :is-collapsed="false"
64
+ :is-mobile="true"
65
+ :items="sidebarItems"
66
+ :is-group="isSidebarGroup"
67
+ @toggle-collapsed="isShowSidebarMobile = false"
68
+ />
69
+ </template>
70
+ </Slideover>
71
+ <NuxtLink :to="routes.home.to">
72
+ <img
73
+ src="/logo-mini.png"
74
+ alt="logo_mini_color"
75
+ class="w-[126px] min-w-[126px] lg:hidden lg:w-[172px] lg:min-w-[172px]"
76
+ />
77
+ </NuxtLink>
78
+ <Apps class="lg:hidden" />
79
+ </div>
80
+
81
+ <div class="flex items-center justify-center gap-4">
82
+ <Notifications :ui="{ content: 'mt-[64px] lg:mt-[72px]' }" />
83
+ <DropdownMenu
84
+ size="xl"
85
+ :items="userMenuItems"
86
+ :content="{
87
+ align: 'end',
88
+ side: 'bottom',
89
+ }"
90
+ :ui="{
91
+ content: 'w-48',
92
+ }"
93
+ >
94
+ <div class="relative flex cursor-pointer items-center gap-2">
95
+ <div class="hidden flex-col justify-end text-right lg:flex">
96
+ <p class="font-bold">
97
+ {{ auth.me.value?.display_name || auth.me.value?.full_name }}
98
+ </p>
99
+ <p class="text-muted text-sm">
100
+ {{ auth.me.value?.email || "" }}
101
+ </p>
102
+ </div>
103
+ <Avatar
104
+ class="border-muted size-[40px] border text-2xl"
105
+ icon="ri:user-line"
106
+ :src="auth.me.value?.avatar_url || ''"
107
+ />
108
+ <Icon
109
+ name="i-ph:caret-down-light"
110
+ class="size-5"
111
+ />
112
+ </div>
113
+ </DropdownMenu>
114
+ </div>
115
+ </nav>
116
+ <div class="w-full bg-gray-50 pt-[64px] lg:pt-[72px]">
117
+ <main
118
+ :class="[
119
+ 'mx-auto min-h-full flex-1 px-6 py-10 lg:px-8',
120
+ fullScreen ? 'w-full 2xl:max-w-[90%]' : 'max-w-7xl 2xl:max-w-[70%]',
121
+ ]"
122
+ >
123
+ <Breadcrumb
124
+ v-if="
125
+ !app.pageMeta.isHideBreadcrumbs && breadcrumbsItems.length > 1
126
+ "
127
+ :items="breadcrumbsItems"
128
+ class="mb-5"
129
+ :ui="{
130
+ item: 'max-w-2/3',
131
+ list: 'w-full',
132
+ }"
133
+ />
134
+ <div
135
+ v-if="app.pageMeta.title"
136
+ class="mb-4 flex flex-col justify-between gap-1 md:mb-6 md:gap-4 lg:flex-row lg:items-start"
137
+ >
138
+ <div class="flex flex-1 flex-col">
139
+ <h1
140
+ class="text-3xl font-bold wrap-break-word lg:max-w-2/3"
141
+ :title="app.pageMeta.title"
142
+ >
143
+ {{ app.pageMeta.title }}
144
+ <span id="page-title-extra" />
145
+ </h1>
146
+
147
+ <div id="page-subtitle" />
148
+ <p
149
+ v-if="app.pageMeta.sub_title"
150
+ class="text-[#475467]"
151
+ >
152
+ {{ app.pageMeta.sub_title }}
153
+ </p>
154
+ </div>
155
+ <div id="page-header" />
156
+ </div>
157
+ <slot />
158
+ </main>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </template>
163
+
164
+ <script lang="ts" setup>
165
+ import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
166
+ import { computed } from 'vue'
167
+ import Sidebar from './Sidebar.vue'
168
+ import Apps from '../Apps.vue'
169
+
170
+ const props = withDefaults(
171
+ defineProps<{
172
+ label: string
173
+ sidebarItems: NavigationMenuItem[]
174
+ isSidebarGroup?: boolean
175
+ fullScreen?: boolean
176
+ rootAppPath?: string
177
+ }>(),
178
+ {
179
+ fullScreen: true,
180
+ },
181
+ )
182
+
183
+ const app = useApp()
184
+ const isShowSidebarMobile = ref(false)
185
+ const auth = useAuth()
186
+ // Cookie to store user preference for desktop
187
+ const isCollapsed = useCookie<boolean>('app.admin.sidebar.isCollapsed', {
188
+ default: () => false,
189
+ path: '/',
190
+ })
191
+
192
+ const userMenuItems: DropdownMenuItem[] = [
193
+ {
194
+ label: 'View profile',
195
+ icon: 'i-lucide-user',
196
+ to: routes.account.profile.to,
197
+ },
198
+
199
+ {
200
+ label: routes.logout.label,
201
+ icon: 'i-lucide-log-out',
202
+ to: routes.logout.to,
203
+ external: true,
204
+ },
205
+ ]
206
+
207
+ const breadcrumbsItems = computed(() => {
208
+ const list = app.pageMeta.breadcrumbs || []
209
+ if (!list.length) return []
210
+ const extra = list.map((item: any) => ({
211
+ ...item,
212
+ icon: '',
213
+ }))
214
+
215
+ return [
216
+ {
217
+ label: '',
218
+ icon: 'ph:house-line',
219
+ to: props.rootAppPath || '/',
220
+ },
221
+ ...extra,
222
+ ]
223
+ })
224
+ </script>
@@ -1,45 +1,45 @@
1
- <template>
2
- <Popover
3
- :content="{
4
- align: 'center',
5
- side: 'bottom',
6
- sideOffset: 8,
7
- }"
8
- >
9
- <Button
10
- icon="mingcute:dot-grid-line"
11
- variant="ghost"
12
- class="text-gray-500"
13
- />
14
-
15
- <template #content>
16
- <Card class="w-[300px]">
17
- <div
18
- class="grid grid-cols-3 gap-6"
19
- >
20
- <Button
21
- v-for="item in auth.menusNavbar.value"
22
- :key="item.to"
23
- class="flex flex-col items-center p-0 text-center"
24
- variant="link"
25
- :to="item.to"
26
- >
27
- <div class="flex h-[42px] items-center justify-center">
28
- <img
29
- :src="item.icon"
30
- class="h-auto w-[32px]"
31
- />
32
- </div>
33
- <span class="text-sm font-medium text-gray-700">
34
- {{ item.label }}
35
- </span>
36
- </Button>
37
- </div>
38
- </Card>
39
- </template>
40
- </Popover>
41
- </template>
42
-
43
- <script lang="ts" setup>
44
- const auth = useAuth()
45
- </script>
1
+ <template>
2
+ <Popover
3
+ :content="{
4
+ align: 'center',
5
+ side: 'bottom',
6
+ sideOffset: 8,
7
+ }"
8
+ >
9
+ <Button
10
+ icon="mingcute:dot-grid-line"
11
+ variant="ghost"
12
+ class="text-gray-500"
13
+ />
14
+
15
+ <template #content>
16
+ <Card class="w-[300px]">
17
+ <div
18
+ class="grid grid-cols-3 gap-6"
19
+ >
20
+ <Button
21
+ v-for="item in auth.menusNavbar.value"
22
+ :key="item.to"
23
+ class="flex flex-col items-center p-0 text-center"
24
+ variant="link"
25
+ :to="item.to"
26
+ >
27
+ <div class="flex h-[42px] items-center justify-center">
28
+ <img
29
+ :src="item.icon"
30
+ class="h-auto w-[32px]"
31
+ />
32
+ </div>
33
+ <span class="text-sm font-medium text-gray-700">
34
+ {{ item.label }}
35
+ </span>
36
+ </Button>
37
+ </div>
38
+ </Card>
39
+ </template>
40
+ </Popover>
41
+ </template>
42
+
43
+ <script lang="ts" setup>
44
+ const auth = useAuth()
45
+ </script>