adminforth 2.8.2 → 2.9.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.
Files changed (70) hide show
  1. package/dist/auth.d.ts +7 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +5 -0
  4. package/dist/auth.js.map +1 -1
  5. package/dist/modules/codeInjector.d.ts.map +1 -1
  6. package/dist/modules/codeInjector.js +18 -2
  7. package/dist/modules/codeInjector.js.map +1 -1
  8. package/dist/modules/configValidator.d.ts.map +1 -1
  9. package/dist/modules/configValidator.js +21 -5
  10. package/dist/modules/configValidator.js.map +1 -1
  11. package/dist/modules/restApi.d.ts.map +1 -1
  12. package/dist/modules/restApi.js +2 -0
  13. package/dist/modules/restApi.js.map +1 -1
  14. package/dist/modules/styles.d.ts +28 -0
  15. package/dist/modules/styles.d.ts.map +1 -1
  16. package/dist/modules/styles.js +28 -0
  17. package/dist/modules/styles.js.map +1 -1
  18. package/dist/modules/utils.d.ts +1 -0
  19. package/dist/modules/utils.d.ts.map +1 -1
  20. package/dist/modules/utils.js +7 -0
  21. package/dist/modules/utils.js.map +1 -1
  22. package/dist/spa/package-lock.json +5 -4
  23. package/dist/spa/package.json +1 -1
  24. package/dist/spa/src/App.vue +43 -176
  25. package/dist/spa/src/adminforth.ts +14 -10
  26. package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
  27. package/dist/spa/src/afcl/Card.vue +25 -0
  28. package/dist/spa/src/afcl/LinkButton.vue +2 -2
  29. package/dist/spa/src/afcl/Table.vue +19 -10
  30. package/dist/spa/src/afcl/VerticalTabs.vue +15 -6
  31. package/dist/spa/src/afcl/index.ts +2 -0
  32. package/dist/spa/src/components/Filters.vue +2 -2
  33. package/dist/spa/src/components/MenuLink.vue +90 -23
  34. package/dist/spa/src/components/Sidebar.vue +443 -0
  35. package/dist/spa/src/components/UserMenuSettingsButton.vue +68 -0
  36. package/dist/spa/src/renderers/CompactField.vue +1 -1
  37. package/dist/spa/src/renderers/CompactUUID.vue +1 -1
  38. package/dist/spa/src/router/index.ts +9 -0
  39. package/dist/spa/src/spa_types/core.ts +5 -0
  40. package/dist/spa/src/stores/filters.ts +29 -2
  41. package/dist/spa/src/types/Back.ts +29 -0
  42. package/dist/spa/src/types/Common.ts +23 -2
  43. package/dist/spa/src/types/FrontendAPI.ts +10 -0
  44. package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
  45. package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
  46. package/dist/spa/src/types/adapters/index.ts +2 -0
  47. package/dist/spa/src/utils.ts +1 -0
  48. package/dist/spa/src/views/ListView.vue +15 -7
  49. package/dist/spa/src/views/LoginView.vue +7 -2
  50. package/dist/spa/src/views/SettingsView.vue +121 -0
  51. package/dist/types/Back.d.ts +38 -0
  52. package/dist/types/Back.d.ts.map +1 -1
  53. package/dist/types/Back.js.map +1 -1
  54. package/dist/types/Common.d.ts +21 -1
  55. package/dist/types/Common.d.ts.map +1 -1
  56. package/dist/types/Common.js.map +1 -1
  57. package/dist/types/FrontendAPI.d.ts +10 -0
  58. package/dist/types/FrontendAPI.d.ts.map +1 -1
  59. package/dist/types/FrontendAPI.js.map +1 -1
  60. package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
  61. package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
  62. package/dist/types/adapters/CaptchaAdapter.js +5 -0
  63. package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
  64. package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
  65. package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
  66. package/dist/types/adapters/KeyValueAdapter.js +2 -0
  67. package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
  68. package/dist/types/adapters/index.d.ts +2 -0
  69. package/dist/types/adapters/index.d.ts.map +1 -1
  70. package/package.json +1 -1
@@ -1,42 +1,109 @@
1
1
  <template>
2
2
  <RouterLink
3
3
  :to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
4
- class="flex group items-center py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover" role="menuitem"
4
+ class="af-menu-link flex group relative items-center w-full py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default transition-all duration-200 ease-in-out"
5
5
  :class="{
6
- 'px-4': isChild,
7
- 'px-2': !isChild,
6
+ 'hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover': !['divider', 'gap', 'heading'].includes(item.type),
7
+ 'pl-6 pr-3.5': (isChild && !isSidebarIconOnly && !isSidebarHovering) || (isChild && isSidebarIconOnly && isSidebarHovering),
8
+ 'px-3.5 ': !isChild || (isSidebarIconOnly && !isSidebarHovering),
9
+ 'max-w-12': isSidebarIconOnly && !isSidebarHovering,
8
10
  'bg-lightSidebarItemActive dark:bg-darkSidebarItemActive': item.resourceId ?
9
11
  ($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
10
12
  ($route.name === item.path)
11
13
  }"
12
14
  >
13
- <component v-if="item.icon" :is="getIcon(item.icon)" class="min-w-5 min-h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
14
- <span class="text-ellipsis overflow-hidden ms-3">{{ item.label }}</span>
15
- <span v-if="item.badge"
15
+ <component v-if="item.icon" :is="getIcon(item.icon)"
16
+ class="min-w-5 min-h-5 text-lightSidebarIcons dark:text-darkSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover transition-all duration-200 ease-in-out"
16
17
  >
17
-
18
- <Tooltip v-if="item.badgeTooltip">
19
- <div class="af-badge inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
20
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
21
-
22
- <template #tooltip>
23
- {{ item.badgeTooltip }}
24
- </template>
25
- </Tooltip>
26
- <template v-else>
27
- <div class="af-badge inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
28
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
29
- </template>
30
-
18
+ </component>
19
+ <div
20
+ class="overflow-hidden block ms-3 pr-4 text-left rtl:text-right transition-all duration-200 ease-in-out"
21
+ :class="{
22
+ 'opacity-0 ms-0 translate-x-4 flex-none': isSidebarIconOnly && !isSidebarHovering,
23
+ 'opacity-100 ms-3 translate-x-0 flex-1': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering),
24
+ }"
25
+ :style="isSidebarIconOnly ? {
26
+ minWidth: isChild
27
+ ? 'calc(16.5rem - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)'
28
+ : 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)',
29
+ width: isChild
30
+ ? 'calc(16.5rem - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)'
31
+ : 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)'
32
+ } : {}"
33
+ >
34
+ {{ item.label }}
35
+ </div>
36
+ <span class="absolute right-1 top-1/2 -translate-y-1/2" v-if="item.badge && showExpandedBadge">
37
+ <Tooltip v-if="item.badgeTooltip">
38
+ <div class="af-badge inline-flex items-center justify-center h-3 py-2.5 px-1 ms-3 text-xs font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
39
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
40
+ <template #tooltip>
41
+ {{ item.badgeTooltip }}
42
+ </template>
43
+ </Tooltip>
44
+ <template v-else>
45
+ <div class="af-badge inline-flex items-center justify-center h-3 py-2.5 px-1 ms-3 text-xs font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
46
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
47
+ </template>
31
48
  </span>
32
-
49
+ <div v-if="item.badge && isSidebarIconOnly && !isSidebarHovering" class="af-badge absolute right-0.5 bottom-1 -translate-y-1/2 inline-flex items-center justify-center h-2 w-2 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
50
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
51
+ </div>
33
52
  </RouterLink>
34
53
  </template>
35
54
 
36
- <script setup lang="ts">
55
+ <script setup lang="ts">
37
56
  import { getIcon } from '@/utils';
38
57
  import { Tooltip } from '@/afcl';
39
- const props = defineProps(['item', 'isChild']);
58
+ import { ref, watch } from 'vue';
59
+
60
+ const props = defineProps(['item', 'isChild', 'isSidebarIconOnly', 'isSidebarHovering']);
61
+
62
+ const BADGE_SHOW_DELAY_MS = 200;
63
+ const showExpandedBadge = ref(false);
64
+ let showBadgeTimer: ReturnType<typeof setTimeout> | null = null;
65
+
66
+ function cancelShowBadgeTimer() {
67
+ if (showBadgeTimer) {
68
+ clearTimeout(showBadgeTimer);
69
+ showBadgeTimer = null;
70
+ }
71
+ }
72
+
73
+ function showBadgeImmediately() {
74
+ cancelShowBadgeTimer();
75
+ showExpandedBadge.value = true;
76
+ }
77
+
78
+ function hideBadgeImmediately() {
79
+ cancelShowBadgeTimer();
80
+ showExpandedBadge.value = false;
81
+ }
82
+
83
+ function showBadgeAfterDelay() {
84
+ cancelShowBadgeTimer();
85
+ showBadgeTimer = setTimeout(() => {
86
+ showExpandedBadge.value = true;
87
+ showBadgeTimer = null;
88
+ }, BADGE_SHOW_DELAY_MS);
89
+ }
90
+
91
+ watch(
92
+ [() => props.isSidebarIconOnly, () => props.isSidebarHovering],
93
+ ([isIconOnly, isHovering]) => {
94
+ if (!isIconOnly) {
95
+ showBadgeImmediately();
96
+ return;
97
+ }
98
+
99
+ if (isHovering) {
100
+ showBadgeAfterDelay();
101
+ return;
102
+ }
40
103
 
104
+ hideBadgeImmediately();
105
+ },
106
+ { immediate: true }
107
+ );
41
108
 
42
109
  </script>
@@ -0,0 +1,443 @@
1
+ <template>
2
+ <aside
3
+ ref="sidebarAside"
4
+ @mouseover="!isTogglingSidebar && (isSidebarHovering = true)"
5
+ @mouseleave="!isTogglingSidebar && (isSidebarHovering = false)"
6
+ id="logo-lightSidebar"
7
+ class="sidebar-container fixed border-none top-0 left-0 z-30 h-screen transition-all duration-300 ease-in-out bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder sm:translate-x-0 dark:border-darkSidebarBorder"
8
+ :class="{
9
+ '-translate-x-full': !sideBarOpen,
10
+ 'transform-none': sideBarOpen,
11
+ 'sidebar-collapsed': iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering,
12
+ 'sidebar-expanded': !iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)
13
+ }"
14
+ aria-label="Sidebar"
15
+ >
16
+ <div class="h-full px-3 pb-4 bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder sidebar-scroll">
17
+ <div class="af-logo-title-wrapper flex ms-2 relative transition-all duration-300 ease-in-out h-8 items-center" :class="{'my-4 ': isSidebarIconOnly && !isSidebarHovering, 'm-4': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
18
+ <img v-if="coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))" :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-logo h-8 me-3" />
19
+ <img v-if="coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering" :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8 me-3" />
20
+ <span
21
+ v-if="coreStore.config?.showBrandNameInSidebar && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))"
22
+ class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
23
+ >
24
+ {{ coreStore.config?.brandName }}
25
+ </span>
26
+ <div class="flex items-center gap-2 w-auto" :class="{'w-full justify-end': coreStore.config?.showBrandLogoInSidebar === false}">
27
+ <component
28
+ v-for="c in coreStore?.config?.globalInjections?.sidebarTop || []"
29
+ :is="getCustomComponent(c)"
30
+ :meta="c.meta"
31
+ :adminUser="coreStore.adminUser"
32
+ />
33
+ </div>
34
+ <div class="absolute top-1.5 -right-4 z-10 hidden sm:block" v-if="!forceIconOnly && iconOnlySidebarEnabled && (!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))">
35
+ <button class="text-sm text-lightSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" @click="toggleSidebar">
36
+ <IconCloseSidebarSolid v-if="!isSidebarIconOnly" class="w-5 h-5 active:scale-95 transition-all duration-200 hover:text-lightSidebarIconsHover dark:hover:text-darkSidebarIconsHover" />
37
+ <IconOpenSidebarSolid v-else class="w-5 h-5 active:scale-95 transition-all duration-200 hover:text-lightSidebarIconsHover dark:hover:text-darkSidebarIconsHover" />
38
+ </button>
39
+ </div>
40
+ </div>
41
+
42
+ <ul class="af-sidebar-container space-y-2 font-medium" >
43
+ <template v-if="!iconOnlySidebarEnabled || !isSidebarIconOnly" v-for="(item, i) in coreStore.menu" :key="`menu-${i}`">
44
+ <div v-if="item.type === 'divider'" class="border-t border-lightSidebarDevider dark:border-darkSidebarDevider"></div>
45
+ <div v-else-if="item.type === 'gap'" class="flex items-center justify-center h-8"></div>
46
+ <div v-else-if="item.type === 'heading'" class="flex items-center justify-left pl-2 h-8 text-lightSidebarHeading dark:text-darkSidebarHeading
47
+ ">{{ item.label }}</div>
48
+ <li v-else-if="item.children" class="af-sidebar-expand-container">
49
+ <button @click="clickOnMenuItem(i)" type="button" class="af-sidebar-expand-button flex items-center w-full px-3.5 py-2 text-base text-lightSidebarText rounded-default transition duration-75 group hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:text-darkSidebarText dark:hover:bg-darkSidebarHover dark:hover:text-darkSidebarTextHover"
50
+ :class="opened.includes(i) ? 'af-sidebar-dropdown-expanded' : 'af-sidebar-dropdown-collapsed'"
51
+ :aria-controls="`dropdown-example${i}`"
52
+ :data-collapse-toggle="`dropdown-example${i}`"
53
+ >
54
+ <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
55
+ <span class="overflow-hidden flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">{{ item.label }}
56
+ <span v-if="item.badge" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
57
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
58
+ <Tooltip v-if="item.badgeTooltip">
59
+ {{ item.badge }}
60
+ <template #tooltip>
61
+ {{ item.badgeTooltip }}
62
+ </template>
63
+ </Tooltip>
64
+ <template v-else>
65
+ {{ item.badge }}
66
+ </template>
67
+ </span>
68
+ </span>
69
+
70
+ <svg :class="{'rotate-180': opened.includes(i) }" class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
71
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
72
+ </svg>
73
+ </button>
74
+
75
+ <ul :id="`dropdown-example${i}`" role="none" class="af-sidebar-dropdown pt-1 space-y-1" :class="{ 'hidden': !opened.includes(i) }">
76
+ <template v-for="(child, j) in item.children" :key="`menu-${i}-${j}`">
77
+ <li class="af-sidebar-menu-link">
78
+ <MenuLink :item="child" isChild="true" @click="$emit('hideSidebar')"/>
79
+ </li>
80
+ </template>
81
+ </ul>
82
+ </li>
83
+ <li v-else class="af-sidebar-menu-link">
84
+ <MenuLink :item="item" @click="$emit('hideSidebar')"/>
85
+ </li>
86
+ </template>
87
+ <template v-if="iconOnlySidebarEnabled && isSidebarIconOnly" v-for="(item, i) in coreStore.menu" :key="`menu-${i}`">
88
+ <div v-if="item.type === 'divider'" class="border-t border-lightSidebarDevider dark:border-darkSidebarDevider"></div>
89
+ <div v-else-if="item.type === 'gap'" class="flex items-center justify-center h-8"></div>
90
+ <div v-else-if="item.type === 'heading' && isSidebarHovering" class="flex items-center justify-left pl-2 h-8 text-lightSidebarHeading dark:text-darkSidebarHeading
91
+ ">{{ item.label }}</div>
92
+ <div v-else-if="item.type === 'heading' && !isSidebarHovering" class="opacity-0 w-1 h-8">{{ item.label }}</div>
93
+ <li v-else-if="item.children" class="af-sidebar-expand-container">
94
+ <button @click="clickOnMenuItem(i)" type="button" class="af-sidebar-expand-button relative flex items-center h-10 w-full px-3.5 py-2 text-base text-lightSidebarText rounded-default group hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:text-darkSidebarText dark:hover:bg-darkSidebarHover dark:hover:text-darkSidebarTextHover"
95
+ :class="opened.includes(i) ? 'af-sidebar-dropdown-expanded' : 'af-sidebar-dropdown-collapsed'"
96
+ :aria-controls="`dropdown-example${i}`"
97
+ :data-collapse-toggle="`dropdown-example${i}`"
98
+ >
99
+ <component v-if="item.icon" :is="getIcon(item.icon)" class="min-w-5 min-h-5 text-lightSidebarIcons group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
100
+
101
+ <span
102
+ class="overflow-hidden block ms-3 text-left rtl:text-right transition-all duration-200 ease-in-out"
103
+ :class="{
104
+ 'opacity-0 ms-0 translate-x-4 flex-none': isSidebarIconOnly && !isSidebarHovering,
105
+ 'opacity-100 ms-3 translate-x-0 flex-none': isSidebarIconOnly && isSidebarHovering,
106
+ 'opacity-100 ms-3 translate-x-0 flex-1': !isSidebarIconOnly
107
+ }"
108
+ :style="isSidebarIconOnly ? {
109
+ minWidth: 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)',
110
+ width: 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)'
111
+ } : {}"
112
+ >{{ item.label }}
113
+
114
+ <span v-if="item.badge" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
115
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
116
+ <Tooltip v-if="item.badgeTooltip">
117
+ {{ item.badge }}
118
+ <template #tooltip>
119
+ {{ item.badgeTooltip }}
120
+ </template>
121
+ </Tooltip>
122
+ <template v-else>
123
+ {{ item.badge }}
124
+ </template>
125
+ </span>
126
+ </span>
127
+
128
+ <svg :class="{'rotate-180': opened.includes(i) }" class="w-3 h-3 ml-2 absolute right-3.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
129
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
130
+ </svg>
131
+ </button>
132
+
133
+ <ul :id="`dropdown-example${i}`" role="none" class="af-sidebar-dropdown pt-1 space-y-1" :class="{ 'hidden': !opened.includes(i), 'relative after:absolute after:-left-3 after:inset-y-0 after:w-0.5 after:bg-lightSidebarIcons/50 dark:after:bg-darkSidebarIcons/50 after:rounded-full': isSidebarIconOnly && !isSidebarHovering && opened.includes(i) }">
134
+ <template v-for="(child, j) in item.children" :key="`menu-${i}-${j}`">
135
+ <li class="af-sidebar-menu-link">
136
+ <MenuLink :item="child" isChild="true" @click="$emit('hideSidebar')" :isSidebarIconOnly="isSidebarIconOnly" :isSidebarHovering="isSidebarHovering"/>
137
+ </li>
138
+ </template>
139
+ </ul>
140
+ </li>
141
+ <li v-else class="af-sidebar-menu-link">
142
+ <MenuLink :item="item" @click="$emit('hideSidebar')" :isSidebarIconOnly="isSidebarIconOnly" :isSidebarHovering="isSidebarHovering"/>
143
+ </li>
144
+ </template>
145
+ </ul>
146
+
147
+
148
+ <div id="dropdown-cta" class="p-4 mt-6 w-[230px] rounded-lg bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
149
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent text-sm" role="alert"
150
+ v-if="(ctaBadge && !isSidebarIconOnly) || (ctaBadge && isSidebarIconOnly && isSidebarHovering)"
151
+ >
152
+ <div class="flex items-center mb-3" :class="!ctaBadge.title ? 'float-right' : ''">
153
+ <!-- <span class="bg-lightPrimaryOpacity dark:bg-darkPrimaryOpacity text-sm font-semibold me-2 px-2.5 py-0.5 rounded "
154
+ v-if="ctaBadge.title"
155
+ > -->
156
+ <span>
157
+ {{ctaBadge.title}}
158
+ </span>
159
+ <button type="button"
160
+ class="ms-auto -mx-1.5 -my-1.5 bg-lightPrimaryOpacity dark:bg-darkPrimaryOpacity inline-flex justify-center items-center w-6 h-6 rounded-lg p-1 hover:brightness-110"
161
+
162
+ data-dismiss-target="#dropdown-cta" aria-label="Close"
163
+ v-if="ctaBadge?.closable" @click="closeCTA"
164
+ >
165
+ <span class="sr-only">Close</span>
166
+ <svg class="w-2.5 h-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
167
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
168
+ </svg>
169
+ </button>
170
+ </div>
171
+ <p class="mb-3 text-sm " v-if="ctaBadge.html" v-html="ctaBadge.html"></p>
172
+ <p class="mb-3 text-sm fill-lightNavbarText dark:fill-darkPrimary text-lightNavbarText dark:text-darkNavbarPrimary" v-else>
173
+ {{ ctaBadge.text }}
174
+ </p>
175
+ <!-- <a class="text-sm text-lightPrimary underline font-medium hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" href="#">Turn new navigation off</a> -->
176
+ </div>
177
+
178
+ <component
179
+ v-for="c in coreStore?.config?.globalInjections?.sidebar || []"
180
+ :is="getCustomComponent(c)"
181
+ :meta="c.meta"
182
+ :adminUser="coreStore.adminUser"
183
+ />
184
+ </div>
185
+ </aside>
186
+ </template>
187
+
188
+ <style lang="scss" scoped>
189
+ /* Sidebar width animations */
190
+ .sidebar-container {
191
+ width: 16rem; /* Default expanded width (w-64) */
192
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
193
+ overflow: hidden; /* Prevent content from showing during animation */
194
+ will-change: width, transform;
195
+ }
196
+
197
+ .sidebar-collapsed {
198
+ width: 5rem; /* Collapsed width (w-18) */
199
+ }
200
+
201
+ .sidebar-expanded {
202
+ width: 16.5rem; /* Expanded width (w-64) */
203
+ }
204
+
205
+ /* Text visibility transitions */
206
+ .sidebar-collapsed .af-title {
207
+ opacity: 0;
208
+ transform: translateX(-12px);
209
+ transition: opacity 0.2s ease-out, transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
210
+ }
211
+ .sidebar-collapsed svg.w-3 {
212
+ opacity: 0;
213
+ transition: opacity 0.2s ease-out;
214
+ }
215
+
216
+ .sidebar-expanded .af-title {
217
+ opacity: 1;
218
+ transform: translateX(0);
219
+ transition: opacity 0.25s ease-in 0.1s, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.05s;
220
+ }
221
+ .sidebar-expanded svg.w-3 {
222
+ opacity: 1;
223
+ transition: opacity 0.25s ease-in 0.1s;
224
+ }
225
+
226
+ /* Overlay scrollbar styling */
227
+ .sidebar-scroll {
228
+ overflow-y: auto;
229
+ overflow-x: hidden;
230
+ scrollbar-width: thin;
231
+ scrollbar-color: transparent transparent;
232
+ }
233
+
234
+ /* Webkit overlay scrollbar */
235
+ .sidebar-scroll::-webkit-scrollbar {
236
+ width: 8px;
237
+ }
238
+
239
+ .sidebar-scroll::-webkit-scrollbar-track {
240
+ background: transparent;
241
+ }
242
+
243
+ .sidebar-scroll::-webkit-scrollbar-thumb {
244
+ background-color: transparent;
245
+ border-radius: 4px;
246
+ transition: background-color 0.2s ease;
247
+ }
248
+
249
+ /* Show scrollbar on hover/scroll */
250
+ .sidebar-scroll:hover::-webkit-scrollbar-thumb,
251
+ .sidebar-scroll:active::-webkit-scrollbar-thumb {
252
+ background-color: rgba(156, 163, 175, 0.4);
253
+ }
254
+
255
+ .sidebar-scroll:hover {
256
+ scrollbar-color: rgba(156, 163, 175, 0.4) transparent;
257
+ }
258
+
259
+ /* Dark mode scrollbar */
260
+ .dark .sidebar-scroll:hover {
261
+ scrollbar-color: rgba(75, 85, 99, 0.4) transparent;
262
+ }
263
+
264
+ .dark .sidebar-scroll:hover::-webkit-scrollbar-thumb,
265
+ .dark .sidebar-scroll:active::-webkit-scrollbar-thumb {
266
+ background-color: rgba(75, 85, 99, 0.4);
267
+ }
268
+
269
+ /* For browsers that support overlay scrollbars natively */
270
+ @supports (overflow: overlay) {
271
+ .sidebar-scroll {
272
+ overflow-y: overlay;
273
+ }
274
+ }
275
+ </style>
276
+
277
+ <script setup lang="ts">
278
+ import { computed, ref, watch, nextTick, onMounted, onUnmounted, type Ref } from 'vue';
279
+ import { useCoreStore } from '@/stores/core';
280
+ import MenuLink from './MenuLink.vue';
281
+ import { IconCloseSidebarSolid, IconOpenSidebarSolid } from '@iconify-prerendered/vue-flowbite';
282
+ import { getIcon, verySimpleHash, loadFile, getCustomComponent } from '@/utils';
283
+ import { Tooltip } from '@/afcl';
284
+ import type { AnnouncementBadgeResponse } from '@/types/Common';
285
+ import adminforth from '@/adminforth';
286
+
287
+ interface Props {
288
+ sideBarOpen: boolean;
289
+ forceIconOnly?: boolean;
290
+ }
291
+
292
+ const props = defineProps<Props>();
293
+
294
+ const emit = defineEmits<{
295
+ hideSidebar: [];
296
+ loadMenu: [];
297
+ sidebarStateChange: [{ isSidebarIconOnly: boolean; isSidebarHovering: boolean }];
298
+ }>();
299
+
300
+ const coreStore = useCoreStore();
301
+
302
+ //create a ref to store the opened menu items with ts type;
303
+ const opened = ref<(string|number)[]>([]);
304
+ const sidebarAside = ref(null);
305
+
306
+ const smQuery = window.matchMedia('(min-width: 640px)');
307
+ const isMobile = ref(!smQuery.matches);
308
+ const iconOnlySidebarEnabled = computed(() => props.forceIconOnly === true || coreStore.config?.iconOnlySidebar?.enabled !== false);
309
+ const isSidebarIconOnly = ref(false);
310
+
311
+ function handleBreakpointChange(e: MediaQueryListEvent) {
312
+ isMobile.value = !e.matches;
313
+ if (isMobile.value) {
314
+ isSidebarIconOnly.value = false;
315
+ } else {
316
+ if (props.forceIconOnly === true) {
317
+ isSidebarIconOnly.value = true;
318
+ } else if (iconOnlySidebarEnabled.value && localStorage.getItem('afIconOnlySidebar') === 'true') {
319
+ isSidebarIconOnly.value = true;
320
+ } else {
321
+ isSidebarIconOnly.value = false;
322
+ }
323
+ }
324
+ }
325
+
326
+ smQuery.addEventListener('change', handleBreakpointChange);
327
+
328
+
329
+ const isSidebarHovering = ref(false);
330
+ const isTogglingSidebar = ref(false);
331
+
332
+ function toggleSidebar() {
333
+ if (props.forceIconOnly) {
334
+ return;
335
+ }
336
+ if (!iconOnlySidebarEnabled.value) {
337
+ return;
338
+ }
339
+ if (isMobile.value) {
340
+ return;
341
+ }
342
+ isTogglingSidebar.value = true;
343
+ isSidebarIconOnly.value = !isSidebarIconOnly.value;
344
+ if (isSidebarIconOnly.value) {
345
+ isSidebarHovering.value = false;
346
+ }
347
+ setTimeout(() => {
348
+ isTogglingSidebar.value = false;
349
+ }, 100);
350
+ }
351
+
352
+ function clickOnMenuItem(label: string | number) {
353
+ if (opened.value.includes(label)) {
354
+ opened.value = opened.value.filter((item) => item !== label);
355
+ } else {
356
+ opened.value.push(label);
357
+ }
358
+ }
359
+
360
+ watch(()=>coreStore.menu, () => {
361
+ coreStore.menu.forEach((item, i) => {
362
+ if (item.open) {
363
+ opened.value.push(i);
364
+ };
365
+ });
366
+ })
367
+
368
+
369
+
370
+ watch(isSidebarIconOnly, (isIconOnly) => {
371
+ if (!isMobile.value && iconOnlySidebarEnabled.value && !props.forceIconOnly) {
372
+ localStorage.setItem('afIconOnlySidebar', isIconOnly.toString());
373
+ }
374
+ emit('sidebarStateChange', { isSidebarIconOnly: isIconOnly, isSidebarHovering: isSidebarHovering.value });
375
+ })
376
+
377
+ watch(isSidebarHovering, (hovering) => {
378
+ emit('sidebarStateChange', { isSidebarIconOnly: isSidebarIconOnly.value, isSidebarHovering: hovering });
379
+ })
380
+
381
+ watch(sidebarAside, (sidebarAside) => {
382
+ if (sidebarAside) {
383
+ coreStore.fetchMenuBadges();
384
+ }
385
+ })
386
+
387
+ const ctaBadge: Ref<(AnnouncementBadgeResponse & { hash: string; }) | null> = computed(() => {
388
+ const badge = coreStore.config?.announcementBadge;
389
+ if (!badge) {
390
+ return null;
391
+ }
392
+ const hash = badge.closable ? verySimpleHash(JSON.stringify(badge)) : '';
393
+ if (badge.closable && window.localStorage.getItem(`ctaBadge-${hash}`)) {
394
+ return null;
395
+ }
396
+ return {...badge, hash};
397
+ });
398
+
399
+ function closeCTA() {
400
+ if (!ctaBadge.value) {
401
+ return;
402
+ }
403
+ const hash = ctaBadge.value.hash;
404
+ window.localStorage.setItem(`ctaBadge-${hash}`, '1');
405
+ nextTick( async() => {
406
+ emit('loadMenu');
407
+ await coreStore.fetchMenuBadges();
408
+ adminforth.menu.refreshMenuBadges();
409
+ })
410
+ }
411
+
412
+ onMounted(() => {
413
+ if (!iconOnlySidebarEnabled.value) {
414
+ isSidebarIconOnly.value = false;
415
+ }
416
+
417
+ coreStore.menu.forEach((item, i) => {
418
+ if (item.open) {
419
+ opened.value.push(i);
420
+ };
421
+ });
422
+ // Emit initial state
423
+ emit('sidebarStateChange', { isSidebarIconOnly: isSidebarIconOnly.value, isSidebarHovering: isSidebarHovering.value });
424
+ })
425
+
426
+ onUnmounted(() => {
427
+ smQuery.removeEventListener('change', handleBreakpointChange);
428
+ })
429
+
430
+ watch(() => props.forceIconOnly, (force) => {
431
+ if (isMobile.value) {
432
+ isSidebarIconOnly.value = false;
433
+ return;
434
+ }
435
+ if (props.forceIconOnly === true) {
436
+ isSidebarIconOnly.value = true;
437
+ } else if (iconOnlySidebarEnabled.value && localStorage.getItem('afIconOnlySidebar') === 'true') {
438
+ isSidebarIconOnly.value = true;
439
+ } else {
440
+ isSidebarIconOnly.value = false;
441
+ }
442
+ }, { immediate: true })
443
+ </script>
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <div class="min-w-40">
3
+ <div class="cursor-pointer flex items-center justify-between gap-1 block px-4 py-2 text-sm text-black
4
+ hover:bg-html dark:text-darkSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextActive
5
+ w-full select-none "
6
+ :class="{ 'bg-black bg-opacity-10 ': showDropdown }"
7
+ @click="showDropdown = !showDropdown"
8
+ >
9
+ <span>Settings</span>
10
+ <IconCaretDownSolid class="h-5 w-5 text-lightPrimary dark:text-gray-400 opacity-50 transition duration-150 ease-in"
11
+ :class="{ 'transform rotate-180': showDropdown }"
12
+ />
13
+ </div>
14
+
15
+ <div v-if="showDropdown" >
16
+
17
+ <router-link class="cursor-pointer flex items-center gap-1 block px-4 py-1 text-sm
18
+ text-black dark:text-darkSidebarTextHover
19
+ bg-black bg-opacity-10
20
+ hover:brightness-110
21
+ hover:text-lightPrimary dark:hover:text-darkPrimary
22
+ hover:bg-lightPrimaryContrast dark:hover:bg-darkPrimaryContrast
23
+ w-full text-select-none pl-5 select-none"
24
+ v-for="option in options"
25
+ :to="getRoute(option)"
26
+ >
27
+ <span class="mr-1">
28
+ <component v-if="option.icon" :is="getIcon(option.icon)" class="w-5 h-5 transition duration-75" ></component>
29
+ </span>
30
+ <span>{{ option.pageLabel }}</span>
31
+ </router-link>
32
+ </div>
33
+
34
+
35
+ </div>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
40
+ import { computed, ref, onMounted, watch } from 'vue';
41
+ import { useCoreStore } from '@/stores/core';
42
+ import { getIcon } from '@/utils';
43
+ import { useRouter } from 'vue-router';
44
+
45
+ const router = useRouter();
46
+ const coreStore = useCoreStore();
47
+
48
+ const showDropdown = ref(false);
49
+ const props = defineProps(['meta', 'resource']);
50
+
51
+ const options = computed(() => {
52
+ return coreStore.config?.settingPages?.map((page) => {
53
+ return {
54
+ pageLabel: page.pageLabel,
55
+ slug: page.slug || null,
56
+ icon: page.icon || null,
57
+ };
58
+ });
59
+ });
60
+
61
+ function getRoute(option: { slug?: string | null, pageLabel: string }) {
62
+ return {
63
+ name: 'settings',
64
+ params: { page: option.slug }
65
+ }
66
+ }
67
+
68
+ </script>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <Tooltip>
3
3
  <span class="flex items-center">
4
- {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
4
+ {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="min-w-5 min-h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
5
5
  </span>
6
6
  <template #tooltip v-if="visualValue">
7
7
  {{ props.record[props.column.name] }}
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <Tooltip>
3
3
  <span class="flex items-center">
4
- {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
4
+ {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="min-w-5 min-h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
5
5
  </span>
6
6
  <template #tooltip v-if="visualValue">
7
7
  {{ props.record[props.column.name] }}