adminforth 3.35.1 → 4.0.1
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.
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +20 -3
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/styleGenerator.d.ts +13 -0
- package/dist/modules/styleGenerator.d.ts.map +1 -1
- package/dist/modules/styleGenerator.js +274 -1
- package/dist/modules/styleGenerator.js.map +1 -1
- package/dist/modules/styles.d.ts +24 -0
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +452 -427
- package/dist/modules/styles.js.map +1 -1
- package/dist/modules/utils.d.ts +1 -13
- package/dist/modules/utils.d.ts.map +1 -1
- package/dist/modules/utils.js +2 -275
- package/dist/modules/utils.js.map +1 -1
- package/dist/spa/index.html +2 -2
- package/dist/spa/package-lock.json +1373 -1136
- package/dist/spa/package.json +18 -15
- package/dist/spa/pnpm-lock.yaml +1067 -648
- package/dist/spa/src/App.vue +84 -92
- package/dist/spa/src/afcl/AreaChart.vue +8 -24
- package/dist/spa/src/afcl/BarChart.vue +8 -23
- package/dist/spa/src/afcl/Button.vue +8 -2
- package/dist/spa/src/afcl/ButtonGroup.vue +9 -9
- package/dist/spa/src/afcl/Card.vue +40 -19
- package/dist/spa/src/afcl/Checkbox.vue +6 -6
- package/dist/spa/src/afcl/CountryFlag.vue +17 -11
- package/dist/spa/src/afcl/DatePicker.vue +7 -7
- package/dist/spa/src/afcl/Dialog.vue +8 -9
- package/dist/spa/src/afcl/DropdownMenu.vue +210 -0
- package/dist/spa/src/afcl/DropdownMenuAccordion.vue +56 -0
- package/dist/spa/src/afcl/DropdownMenuItem.vue +86 -0
- package/dist/spa/src/afcl/Dropzone.vue +10 -26
- package/dist/spa/src/afcl/Input.vue +4 -4
- package/dist/spa/src/afcl/Link.vue +1 -1
- package/dist/spa/src/afcl/LinkButton.vue +15 -1
- package/dist/spa/src/afcl/MixedChart.vue +4 -18
- package/dist/spa/src/afcl/Modal.vue +9 -9
- package/dist/spa/src/afcl/PieChart.vue +8 -29
- package/dist/spa/src/afcl/ProgressBar.vue +2 -2
- package/dist/spa/src/afcl/Select.vue +20 -20
- package/dist/spa/src/afcl/Skeleton.vue +1 -1
- package/dist/spa/src/afcl/Spinner.vue +1 -1
- package/dist/spa/src/afcl/Table.vue +19 -26
- package/dist/spa/src/afcl/Textarea.vue +1 -1
- package/dist/spa/src/afcl/Toggle.vue +4 -4
- package/dist/spa/src/afcl/Tooltip.vue +1 -1
- package/dist/spa/src/afcl/TreeMapChart.vue +5 -20
- package/dist/spa/src/afcl/VerticalTabs.vue +4 -4
- package/dist/spa/src/afcl/buttonStyles.ts +73 -33
- package/dist/spa/src/afcl/chartUtils.ts +19 -0
- package/dist/spa/src/afcl/classMerge.ts +11 -0
- package/dist/spa/src/afcl/dropdownMenuContext.ts +10 -0
- package/dist/spa/src/afcl/fileSize.ts +12 -0
- package/dist/spa/src/afcl/index.ts +3 -0
- package/dist/spa/src/components/AcceptModal.vue +17 -19
- package/dist/spa/src/components/Breadcrumbs.vue +11 -13
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +2 -2
- package/dist/spa/src/components/CallActionWrapper.vue +3 -0
- package/dist/spa/src/components/ColumnValueInput.vue +5 -5
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +7 -6
- package/dist/spa/src/components/CustomDateRangePicker.vue +8 -8
- package/dist/spa/src/components/CustomRangePicker.vue +2 -2
- package/dist/spa/src/components/ErrorMessage.vue +3 -4
- package/dist/spa/src/components/Filters.vue +13 -13
- package/dist/spa/src/components/GroupsTable.vue +12 -12
- package/dist/spa/src/components/ListActionsThreeDots.vue +89 -207
- package/dist/spa/src/components/MenuLink.vue +23 -20
- package/dist/spa/src/components/RangePicker.vue +9 -10
- package/dist/spa/src/components/ResourceListTable.vue +68 -65
- package/dist/spa/src/components/ShowTable.vue +8 -8
- package/dist/spa/src/components/Sidebar.vue +31 -38
- package/dist/spa/src/components/ThreeDotsMenu.vue +88 -136
- package/dist/spa/src/components/Toast.vue +16 -23
- package/dist/spa/src/components/UserMenuSettingsButton.vue +15 -36
- package/dist/spa/src/components/ValueRenderer.vue +13 -8
- package/dist/spa/src/components/icons/IconCalendar.vue +3 -3
- package/dist/spa/src/components/icons/IconTime.vue +3 -3
- package/dist/spa/src/index.scss +6 -2
- package/dist/spa/src/main.ts +2 -0
- package/dist/spa/src/renderers/CompactField.vue +3 -3
- package/dist/spa/src/renderers/CompactUUID.vue +3 -3
- package/dist/spa/src/renderers/SensitiveBlurCell.vue +9 -9
- package/dist/spa/src/utils/threeDotsMenu.ts +7 -0
- package/dist/spa/src/utils/userMenu.ts +5 -0
- package/dist/spa/src/utils/utils.ts +12 -39
- package/dist/spa/src/views/CreateEditSkeleton.vue +3 -3
- package/dist/spa/src/views/CreateView.vue +5 -6
- package/dist/spa/src/views/EditView.vue +5 -6
- package/dist/spa/src/views/ListView.vue +114 -115
- package/dist/spa/src/views/LoginView.vue +4 -4
- package/dist/spa/src/views/ShowView.vue +17 -18
- package/dist/spa/vite.config.ts +23 -16
- package/package.json +1 -2
package/dist/spa/src/App.vue
CHANGED
|
@@ -3,19 +3,17 @@
|
|
|
3
3
|
<nav
|
|
4
4
|
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout"
|
|
5
5
|
id="af-header-nav"
|
|
6
|
-
class="fixed h-14 top-0 z-30 w-full border-b
|
|
6
|
+
class="fixed h-14 top-0 z-30 w-full border-b border-lightNavbarBorder bg-lightNavbar dark:bg-darkNavbar dark:border-darkNavbarBorder"
|
|
7
7
|
>
|
|
8
8
|
<div class="af-header px-3 lg:px-5 lg:pl-3 flex items-center justify-between h-full w-full" >
|
|
9
9
|
<div class="flex items-center justify-start rtl:justify-end">
|
|
10
10
|
<button @click="sideBarOpen = !sideBarOpen"
|
|
11
|
-
type="button" class="inline-flex items-center
|
|
11
|
+
type="button" class="inline-flex items-center justify-center w-[2.125rem] h-[2.125rem] text-sm text-lightNavbarIcons rounded-default sm:hidden hover:bg-lightSidebarItemHover focus:outline-none focus-visible:ring-[3px] focus-visible:ring-lightFocusRing dark:text-darkNavbarIcons dark:hover:bg-darkSidebarItemHover dark:focus-visible:ring-darkFocusRing">
|
|
12
12
|
<span class="sr-only">{{ $t('Open sidebar') }}</span>
|
|
13
|
-
<
|
|
14
|
-
<path clip-rule="evenodd" fill-rule="evenodd" d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"></path>
|
|
15
|
-
</svg>
|
|
13
|
+
<IconMenu class="w-4 h-4" aria-hidden="true" />
|
|
16
14
|
</button>
|
|
17
15
|
</div>
|
|
18
|
-
<div class="flex items-center gap-
|
|
16
|
+
<div class="flex items-center gap-3">
|
|
19
17
|
<component
|
|
20
18
|
v-if="coreStore?.adminUser"
|
|
21
19
|
v-for="c in coreStore?.config?.globalInjections?.header || []"
|
|
@@ -25,66 +23,78 @@
|
|
|
25
23
|
/>
|
|
26
24
|
|
|
27
25
|
<Tooltip v-if="coreStore.isInternetError">
|
|
28
|
-
<IconWifiOff class="blinking-icon w-
|
|
26
|
+
<IconWifiOff class="blinking-icon w-5 h-5 text-lightInputErrorColor dark:text-darkInputErrorColor" />
|
|
29
27
|
<template #tooltip>
|
|
30
28
|
{{$t('Internet connection lost')}}
|
|
31
29
|
</template>
|
|
32
30
|
</Tooltip>
|
|
33
31
|
|
|
34
|
-
<
|
|
32
|
+
<button
|
|
35
33
|
v-if="!coreStore.config?.singleTheme"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
</div>
|
|
60
|
-
<Teleport to="body">
|
|
61
|
-
<div class="af-settings-popup z-50 hidden my-4 max-h-[calc(100dvh-5rem)] overflow-y-auto overscroll-contain text-base list-none bg-lightUserMenuBackground divide-y divide-lightUserMenuBorder text-lightUserMenuText rounded-default shadow dark:shadow-black dark:bg-darkUserMenuBackground dark:divide-darkUserMenuBorder text-darkUserMenuText dark:shadow-black" id="dropdown-user">
|
|
62
|
-
<div class="px-4 py-3" role="none">
|
|
63
|
-
<p class="text-sm text-gray-900 dark:text-darkNavbarText" role="none" v-if="coreStore.userFullname">
|
|
64
|
-
{{ coreStore.userFullname }}
|
|
65
|
-
</p>
|
|
66
|
-
<p class="text-sm font-medium text-gray-900 truncate dark:text-darkSidebarText" role="none">
|
|
67
|
-
{{ coreStore.username }}
|
|
68
|
-
</p>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
<ul class="py-1" role="none">
|
|
72
|
-
<li v-for="c in userMenuComponents" class="bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover" >
|
|
73
|
-
<component
|
|
74
|
-
:is="getCustomComponent(c)"
|
|
75
|
-
:meta="c.meta"
|
|
76
|
-
:adminUser="coreStore.adminUser"
|
|
34
|
+
type="button"
|
|
35
|
+
:aria-label="$t('Toggle theme')"
|
|
36
|
+
@click="toggleTheme"
|
|
37
|
+
class="af-header-theme-toggle flex items-center justify-center w-[2.125rem] h-[2.125rem] rounded-default cursor-pointer text-lightNavbarIcons hover:text-lightNavbarText hover:bg-lightSidebarItemHover focus:outline-none focus-visible:ring-[3px] focus-visible:ring-lightFocusRing dark:text-darkNavbarIcons dark:hover:text-darkNavbarText dark:hover:bg-darkSidebarItemHover dark:focus-visible:ring-darkFocusRing transition-colors">
|
|
38
|
+
<IconMoon class="w-4 h-4" v-if="coreStore.theme !== 'dark'" />
|
|
39
|
+
<IconSun class="w-4 h-4" v-else />
|
|
40
|
+
</button>
|
|
41
|
+
|
|
42
|
+
<DropdownMenu
|
|
43
|
+
v-model:open="userMenuOpen"
|
|
44
|
+
placement="bottom-end"
|
|
45
|
+
keepMounted
|
|
46
|
+
contentClass="af-settings-popup min-w-[15rem] max-w-none max-h-[calc(100dvh-5rem)] overflow-y-auto overscroll-contain divide-y bg-lightUserMenuBackground divide-lightUserMenuBorder text-lightUserMenuText dark:bg-darkUserMenuBackground dark:divide-darkUserMenuBorder dark:text-darkUserMenuText"
|
|
47
|
+
>
|
|
48
|
+
<template #trigger>
|
|
49
|
+
<button type="button" class="flex text-sm rounded-full focus:outline-none focus-visible:ring-[3px] focus-visible:ring-lightFocusRing dark:focus-visible:ring-darkFocusRing" :aria-expanded="userMenuOpen">
|
|
50
|
+
<span class="sr-only">{{ $t('Open user menu') }}</span>
|
|
51
|
+
<span class="flex rounded-full ring-1 ring-lightNavbarBorder hover:ring-lightInputBorderHover dark:ring-darkNavbarBorder dark:hover:ring-darkInputBorderHover transition-shadow">
|
|
52
|
+
<img
|
|
53
|
+
v-if="coreStore.userAvatarUrl"
|
|
54
|
+
class="w-[2.125rem] h-[2.125rem] rounded-full object-cover"
|
|
55
|
+
:src="coreStore.userAvatarUrl"
|
|
56
|
+
alt="user photo"
|
|
77
57
|
/>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
</
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
58
|
+
<span v-else class="flex items-center justify-center w-[2.125rem] h-[2.125rem] rounded-full bg-lightSecondaryDarken text-lightNavbarIcons dark:bg-darkSecondaryLighten dark:text-darkNavbarIcons" aria-hidden="true">
|
|
59
|
+
<IconUser class="w-4 h-4" />
|
|
60
|
+
</span>
|
|
61
|
+
</span>
|
|
62
|
+
</button>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<div class="px-3 pt-2 pb-2.5" role="none">
|
|
66
|
+
<p class="text-sm font-medium text-lightUserMenuText truncate dark:text-darkUserMenuText" role="none" v-if="coreStore.userFullname">
|
|
67
|
+
{{ coreStore.userFullname }}
|
|
68
|
+
</p>
|
|
69
|
+
<p class="text-xs text-lightUserMenuText/60 truncate dark:text-darkUserMenuText/60" role="none" :class="{ 'text-sm font-medium !text-lightUserMenuText dark:!text-darkUserMenuText': !coreStore.userFullname }">
|
|
70
|
+
{{ coreStore.username }}
|
|
71
|
+
</p>
|
|
86
72
|
</div>
|
|
87
|
-
|
|
73
|
+
|
|
74
|
+
<ul class="py-1" role="none">
|
|
75
|
+
<!-- plain injected items get the hover from the row, AFCL menu items highlight themselves -->
|
|
76
|
+
<li v-for="c in userMenuComponents" class="rounded-md overflow-hidden bg-lightUserMenuItemBackground text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:text-darkUserMenuItemText [&:not(:has(.afcl-dropdown-menu-item))]:hover:bg-lightUserMenuItemBackgroundHover dark:[&:not(:has(.afcl-dropdown-menu-item))]:hover:bg-darkUserMenuItemBackgroundHover" >
|
|
77
|
+
<component
|
|
78
|
+
:is="getCustomComponent(c)"
|
|
79
|
+
:meta="c.meta"
|
|
80
|
+
:adminUser="coreStore.adminUser"
|
|
81
|
+
/>
|
|
82
|
+
</li>
|
|
83
|
+
<li v-if="coreStore?.config?.settingPages && coreStore.config.settingPages.length > 0">
|
|
84
|
+
<UserMenuSettingsButton />
|
|
85
|
+
</li>
|
|
86
|
+
<li class="mt-1 pt-1 border-t border-lightUserMenuBorder dark:border-darkUserMenuBorder first:mt-0 first:pt-0 first:border-t-0">
|
|
87
|
+
<DropdownMenuItem
|
|
88
|
+
danger
|
|
89
|
+
:class="USER_MENU_ITEM_CLASSES"
|
|
90
|
+
@click="logout"
|
|
91
|
+
>
|
|
92
|
+
<template #icon><IconLogOut /></template>
|
|
93
|
+
{{ $t('Sign out') }}
|
|
94
|
+
</DropdownMenuItem>
|
|
95
|
+
</li>
|
|
96
|
+
</ul>
|
|
97
|
+
</DropdownMenu>
|
|
88
98
|
</div>
|
|
89
99
|
</div>
|
|
90
100
|
</nav>
|
|
@@ -106,7 +116,7 @@
|
|
|
106
116
|
maxWidth: headerOnlyLayout ? '100%' : isSidebarIconOnly ? 'calc(100% - 4.5rem)' : `calc(100% - ${expandedWidth})`
|
|
107
117
|
}"
|
|
108
118
|
>
|
|
109
|
-
<div class="p-0
|
|
119
|
+
<div class="p-0 mt-14">
|
|
110
120
|
<RouterView/>
|
|
111
121
|
</div>
|
|
112
122
|
</div>
|
|
@@ -116,7 +126,7 @@
|
|
|
116
126
|
</div>
|
|
117
127
|
|
|
118
128
|
<div v-else class="flex items-center justify-center h-screen">
|
|
119
|
-
<div class="text-3xl text-
|
|
129
|
+
<div class="text-3xl text-lightListTablePaginationHelpText dark:text-darkListTablePaginationHelpText animate-bounce">
|
|
120
130
|
<Spinner class="w-8 h-8" />
|
|
121
131
|
</div>
|
|
122
132
|
</div>
|
|
@@ -134,7 +144,7 @@
|
|
|
134
144
|
|
|
135
145
|
<div v-if="sideBarOpen"
|
|
136
146
|
@click="hideSidebar"
|
|
137
|
-
drawer-backdrop="" class="bg-
|
|
147
|
+
drawer-backdrop="" class="bg-zinc-900/30 dark:bg-black/60 fixed inset-0 z-20">
|
|
138
148
|
</div>
|
|
139
149
|
|
|
140
150
|
<component
|
|
@@ -188,14 +198,12 @@
|
|
|
188
198
|
</style>
|
|
189
199
|
|
|
190
200
|
<script setup lang="ts">
|
|
191
|
-
import { computed,
|
|
201
|
+
import { computed, onMounted, onUnmounted, ref, watch, onBeforeMount } from 'vue';
|
|
192
202
|
import { RouterView } from 'vue-router';
|
|
193
|
-
import { Dropdown } from 'flowbite'
|
|
194
203
|
import './index.scss'
|
|
195
204
|
import { useCoreStore } from '@/stores/core';
|
|
196
205
|
import { useUserStore } from '@/stores/user';
|
|
197
|
-
import {
|
|
198
|
-
import { IconWifiOff } from '@iconify-prerendered/vue-humbleicons';
|
|
206
|
+
import { IconMoon, IconSun, IconWifiOff, IconMenu, IconUser, IconLogOut } from '@iconify-prerendered/vue-lucide';
|
|
199
207
|
import AcceptModal from './components/AcceptModal.vue';
|
|
200
208
|
import Sidebar from './components/Sidebar.vue';
|
|
201
209
|
import { useRoute, useRouter } from 'vue-router';
|
|
@@ -203,17 +211,16 @@ import { createHead } from 'unhead'
|
|
|
203
211
|
import { getCustomComponent, handleNotAuthorized } from '@/utils';
|
|
204
212
|
import Toast from './components/Toast.vue';
|
|
205
213
|
import {useToastStore} from '@/stores/toast';
|
|
206
|
-
import { initFrontedAPI } from '@/adminforth';
|
|
207
|
-
import { useAdminforth } from '@/adminforth';
|
|
214
|
+
import adminforth, { initFrontedAPI, useAdminforth } from '@/adminforth';
|
|
208
215
|
import UserMenuSettingsButton from './components/UserMenuSettingsButton.vue';
|
|
209
|
-
import { Tooltip, Spinner } from '@/afcl'
|
|
216
|
+
import { Tooltip, Spinner, DropdownMenu, DropdownMenuItem } from '@/afcl'
|
|
217
|
+
import { USER_MENU_ITEM_CLASSES } from '@/utils/userMenu';
|
|
210
218
|
|
|
211
219
|
const coreStore = useCoreStore();
|
|
212
220
|
const toastStore = useToastStore();
|
|
213
221
|
const userStore = useUserStore();
|
|
214
222
|
|
|
215
223
|
const { menu } = useAdminforth();
|
|
216
|
-
let { closeUserMenuDropdown } = useAdminforth();
|
|
217
224
|
|
|
218
225
|
initFrontedAPI()
|
|
219
226
|
|
|
@@ -222,7 +229,12 @@ const sideBarOpen = ref(false);
|
|
|
222
229
|
const route = useRoute();
|
|
223
230
|
const router = useRouter();
|
|
224
231
|
const publicConfigLoaded = ref(false);
|
|
225
|
-
const
|
|
232
|
+
const userMenuOpen = ref(false);
|
|
233
|
+
|
|
234
|
+
// injected user menu items close the menu via useAdminforth().closeUserMenuDropdown()
|
|
235
|
+
adminforth.closeUserMenuDropdown = () => {
|
|
236
|
+
userMenuOpen.value = false;
|
|
237
|
+
};
|
|
226
238
|
|
|
227
239
|
|
|
228
240
|
const routerIsReady = ref(false);
|
|
@@ -342,6 +354,7 @@ watch(title, (title) => {
|
|
|
342
354
|
})
|
|
343
355
|
|
|
344
356
|
watch(route, () => {
|
|
357
|
+
userMenuOpen.value = false;
|
|
345
358
|
// Handle preferIconOnly layout
|
|
346
359
|
if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
|
|
347
360
|
isSidebarIconOnly.value = true;
|
|
@@ -349,31 +362,10 @@ watch(route, () => {
|
|
|
349
362
|
});
|
|
350
363
|
|
|
351
364
|
|
|
352
|
-
watch(dropdownUserButton, async (dropdownUserButton) => {
|
|
353
|
-
if (dropdownUserButton) {
|
|
354
|
-
await nextTick();
|
|
355
|
-
const dropdownUser = document.querySelector('#dropdown-user') as HTMLElement;
|
|
356
|
-
// trigger is passed by ref without data-dropdown-toggle: flowbite auto-inits such triggers on window load
|
|
357
|
-
// with default 'bottom' placement, and that second popper overrides our positioning
|
|
358
|
-
const dd = new Dropdown(
|
|
359
|
-
dropdownUser,
|
|
360
|
-
dropdownUserButton,
|
|
361
|
-
{ placement: 'bottom-end' },
|
|
362
|
-
);
|
|
363
|
-
// trigger lives in the fixed header, so fixed strategy keeps popup in place while the page scrolls
|
|
364
|
-
dd._popperInstance.setOptions({ strategy: 'fixed' });
|
|
365
|
-
// keep popup anchored to the right edge when its content size changes (e.g. async user menu components)
|
|
366
|
-
new ResizeObserver(() => dd._popperInstance.update()).observe(dropdownUser);
|
|
367
|
-
closeUserMenuDropdown = () => {
|
|
368
|
-
dd.hide();
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
})
|
|
372
|
-
|
|
373
365
|
// initialize components based on data attribute selectors
|
|
374
366
|
onMounted(async () => {
|
|
375
367
|
await loadConfig();
|
|
376
|
-
//
|
|
368
|
+
// wait for the router: it affects the dom (our v-ifs) and fetches the menu
|
|
377
369
|
await initRouter();
|
|
378
370
|
document.documentElement.setAttribute('data-theme', theme.value);
|
|
379
371
|
menu.refresh = async () => {
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
<script setup lang="ts">
|
|
6
6
|
import type ApexCharts from 'apexcharts';
|
|
7
|
-
import {
|
|
7
|
+
import type { ApexOptions } from 'apexcharts';
|
|
8
8
|
import { ref, type Ref, watch, computed, onUnmounted } from 'vue';
|
|
9
|
+
import { mergeChartOptions } from './chartUtils';
|
|
9
10
|
type ApexChartsConstructor = new (el: Element, options: any) => ApexCharts;
|
|
10
11
|
|
|
11
12
|
const chart: Ref<HTMLDivElement | null> = ref(null);
|
|
@@ -29,7 +30,7 @@ const optionsBase = {
|
|
|
29
30
|
chart: {
|
|
30
31
|
height: 150,
|
|
31
32
|
type: "area",
|
|
32
|
-
fontFamily: "
|
|
33
|
+
fontFamily: "'Geist Variable', ui-sans-serif, sans-serif",
|
|
33
34
|
dropShadow: {
|
|
34
35
|
enabled: false,
|
|
35
36
|
},
|
|
@@ -46,17 +47,17 @@ const optionsBase = {
|
|
|
46
47
|
fill: {
|
|
47
48
|
type: "gradient",
|
|
48
49
|
gradient: {
|
|
49
|
-
opacityFrom: 0.
|
|
50
|
+
opacityFrom: 0.18,
|
|
50
51
|
opacityTo: 0,
|
|
51
|
-
shade: "#
|
|
52
|
-
gradientToColors: ["#
|
|
52
|
+
shade: "#2a78d6",
|
|
53
|
+
gradientToColors: ["#2a78d6"],
|
|
53
54
|
},
|
|
54
55
|
},
|
|
55
56
|
dataLabels: {
|
|
56
57
|
enabled: false,
|
|
57
58
|
},
|
|
58
59
|
stroke: {
|
|
59
|
-
width:
|
|
60
|
+
width: 2,
|
|
60
61
|
},
|
|
61
62
|
grid: {
|
|
62
63
|
show: false,
|
|
@@ -116,24 +117,7 @@ const options = computed(() => {
|
|
|
116
117
|
},
|
|
117
118
|
};
|
|
118
119
|
|
|
119
|
-
|
|
120
|
-
function mergeOptions(options: any, newOptions: any) {
|
|
121
|
-
if (!newOptions) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
for (const key in newOptions) {
|
|
125
|
-
// and is not array
|
|
126
|
-
if (typeof newOptions[key] === 'object' && !Array.isArray(newOptions[key])) {
|
|
127
|
-
if (!options[key]) {
|
|
128
|
-
options[key] = {};
|
|
129
|
-
}
|
|
130
|
-
mergeOptions(options[key], newOptions[key]);
|
|
131
|
-
} else {
|
|
132
|
-
options[key] = newOptions[key];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
mergeOptions(options, props.options);
|
|
120
|
+
mergeChartOptions(options, props.options);
|
|
137
121
|
|
|
138
122
|
return options;
|
|
139
123
|
});
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
<script setup lang="ts">
|
|
6
6
|
import type ApexCharts from 'apexcharts';
|
|
7
|
-
import {
|
|
7
|
+
import type { ApexOptions } from 'apexcharts';
|
|
8
8
|
import { ref, type Ref, watch, computed, onUnmounted } from 'vue';
|
|
9
|
+
import { CHART_COLORS, mergeChartOptions } from './chartUtils';
|
|
9
10
|
type ApexChartsConstructor = new (el: Element, options: any) => ApexCharts;
|
|
10
11
|
|
|
11
12
|
const chart: Ref<HTMLDivElement | null> = ref(null);
|
|
@@ -18,7 +19,7 @@ const props = defineProps<{
|
|
|
18
19
|
series: {
|
|
19
20
|
name: string,
|
|
20
21
|
fieldName: string,
|
|
21
|
-
color
|
|
22
|
+
color?: string,
|
|
22
23
|
}[],
|
|
23
24
|
options: ApexOptions,
|
|
24
25
|
}>();
|
|
@@ -70,7 +71,7 @@ const optionsBase = {
|
|
|
70
71
|
labels: {
|
|
71
72
|
show: false,
|
|
72
73
|
style: {
|
|
73
|
-
fontFamily: "
|
|
74
|
+
fontFamily: "'Geist Variable', ui-sans-serif, sans-serif",
|
|
74
75
|
cssClass: 'text-xs font-normal fill-gray-500 dark:fill-gray-400'
|
|
75
76
|
},
|
|
76
77
|
formatter: function (value: any) {
|
|
@@ -89,7 +90,7 @@ const optionsBase = {
|
|
|
89
90
|
labels: {
|
|
90
91
|
show: false,
|
|
91
92
|
style: {
|
|
92
|
-
fontFamily: "
|
|
93
|
+
fontFamily: "'Geist Variable', ui-sans-serif, sans-serif",
|
|
93
94
|
cssClass: 'text-xs font-normal fill-gray-500 dark:fill-gray-400'
|
|
94
95
|
}
|
|
95
96
|
}
|
|
@@ -119,9 +120,10 @@ const options = computed(() => {
|
|
|
119
120
|
...optionsBase,
|
|
120
121
|
|
|
121
122
|
// shade and gradient take from first series
|
|
122
|
-
series: props.series.map((s) => ({
|
|
123
|
+
series: props.series.map((s, index) => ({
|
|
123
124
|
data: props.data?.map((item) => item[s.fieldName]) ?? [],
|
|
124
125
|
...s,
|
|
126
|
+
color: s.color ?? CHART_COLORS[index],
|
|
125
127
|
})),
|
|
126
128
|
xaxis: {
|
|
127
129
|
...optionsBase.xaxis,
|
|
@@ -129,24 +131,7 @@ const options = computed(() => {
|
|
|
129
131
|
},
|
|
130
132
|
};
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
function mergeOptions(options: any, newOptions: any) {
|
|
134
|
-
if (!newOptions) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
for (const key in newOptions) {
|
|
138
|
-
// and is not array
|
|
139
|
-
if (typeof newOptions[key] === 'object' && !Array.isArray(newOptions[key])) {
|
|
140
|
-
if (!options[key]) {
|
|
141
|
-
options[key] = {};
|
|
142
|
-
}
|
|
143
|
-
mergeOptions(options[key], newOptions[key]);
|
|
144
|
-
} else {
|
|
145
|
-
options[key] = newOptions[key];
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
mergeOptions(options, props.options);
|
|
134
|
+
mergeChartOptions(options, props.options);
|
|
150
135
|
|
|
151
136
|
return options;
|
|
152
137
|
});
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
<button
|
|
3
3
|
type="submit"
|
|
4
4
|
v-bind="restAttrs"
|
|
5
|
+
:disabled="props.disabled"
|
|
5
6
|
:class="buttonClasses"
|
|
6
7
|
>
|
|
7
8
|
<Spinner v-if="props.loader" class="w-4 h-4 text-lightButtonsText dark:text-darkButtonsText fill-lightPrimary dark:fill-darkPrimary" />
|
|
8
|
-
|
|
9
|
+
<!-- the spinner takes the place of the icon in a square button -->
|
|
10
|
+
<slot v-if="!(props.iconOnly && props.loader)"></slot>
|
|
9
11
|
</button>
|
|
10
12
|
</template>
|
|
11
13
|
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
import { Spinner } from '@/afcl';
|
|
14
16
|
import { computed, useAttrs } from 'vue';
|
|
15
17
|
import { type ClassNameValue } from 'tailwind-merge';
|
|
16
|
-
import { getButtonClasses, type ButtonVariant } from './buttonStyles';
|
|
18
|
+
import { getButtonClasses, type ButtonVariant, type ButtonSize } from './buttonStyles';
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
defineOptions({ inheritAttrs: false });
|
|
@@ -30,6 +32,8 @@ const props = withDefaults(defineProps<{
|
|
|
30
32
|
disabled?: boolean;
|
|
31
33
|
active?: boolean;
|
|
32
34
|
variant?: ButtonVariant;
|
|
35
|
+
size?: ButtonSize;
|
|
36
|
+
iconOnly?: boolean;
|
|
33
37
|
/** @deprecated use variant instead of mode */
|
|
34
38
|
mode?: ButtonVariant;
|
|
35
39
|
shadow?: boolean;
|
|
@@ -45,6 +49,8 @@ const currentVariant = computed(() => props.variant ?? props.mode ?? 'primary');
|
|
|
45
49
|
const buttonClasses = computed(() => getButtonClasses(
|
|
46
50
|
{
|
|
47
51
|
variant: currentVariant.value,
|
|
52
|
+
size: props.size,
|
|
53
|
+
iconOnly: props.iconOnly,
|
|
48
54
|
shadow: props.shadow,
|
|
49
55
|
active: props.active,
|
|
50
56
|
disabled: props.disabled,
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="isInitFinished" class="inline-flex rounded-
|
|
2
|
+
<div v-if="isInitFinished" class="inline-flex rounded-default shadow-customLight" role="group">
|
|
3
3
|
<button v-for="button in buttons" :key="`${button}-button-controll`"
|
|
4
4
|
:disabled="slotProps[button].disabled"
|
|
5
|
-
class="inline-flex items-center text-sm font-medium border-t border-b border-r border-lightButtonGroupBorder focus:z-10 focus:ring-
|
|
5
|
+
class="inline-flex items-center text-sm font-medium border-t border-b border-r border-lightButtonGroupBorder focus-visible:z-10 focus-visible:ring-[3px] dark:border-darkButtonGroupBorder disabled:opacity-50 disabled:cursor-not-allowed"
|
|
6
6
|
:class="[
|
|
7
|
-
buttonsStyles[button] === 'rounded' ? 'border rounded-
|
|
8
|
-
: buttonsStyles[button] === 'rounded-left' ? 'border rounded-s-
|
|
9
|
-
: buttonsStyles[button] === 'rounded-right' ? 'border rounded-e-
|
|
10
|
-
: buttonsStyles[button] === 'no-rounded' ? 'border border-l-lightButtonGroupBackground focus:border-l-lightButtonGroupBorder dark:border-l-darkButtonGroupBackground dark:focus:border-l-darkButtonGroupBorder'
|
|
11
|
-
: buttonsStyles[button] === 'rounded-left-with-right-border' ? 'border rounded-s-
|
|
12
|
-
(button === activeButton || props.solidColor === true) ? 'bg-lightButtonGroupActiveBackground text-lightButtonGroupActiveText focus:ring-lightButtonGroupActiveFocusRing dark:bg-darkButtonGroupActiveBackground dark:text-darkButtonGroupActiveText dark:focus:ring-darkButtonGroupActiveFocusRing'
|
|
13
|
-
:'text-lightButtonGroupText bg-lightButtonGroupBackground focus:ring-lightButtonGroupFocusRing hover:bg-lightButtonGroupBackgroundHover hover:text-lightButtonGroupTextHover dark:bg-darkButtonGroupBackground dark:text-darkButtonGroupText dark:hover:text-darkButtonGroupTextHover dark:hover:bg-darkButtonGroupBackgroundHover dark:focus:ring-darkButtonGroupFocusRing'
|
|
7
|
+
buttonsStyles[button] === 'rounded' ? 'border rounded-default'
|
|
8
|
+
: buttonsStyles[button] === 'rounded-left' ? 'border rounded-s-default'
|
|
9
|
+
: buttonsStyles[button] === 'rounded-right' ? 'border rounded-e-default border-l-lightButtonGroupBackground focus-visible:border-l-lightButtonGroupBorder dark:border-l-darkButtonGroupBackground dark:focus-visible:border-l-darkButtonGroupBorder'
|
|
10
|
+
: buttonsStyles[button] === 'no-rounded' ? 'border border-l-lightButtonGroupBackground focus-visible:border-l-lightButtonGroupBorder dark:border-l-darkButtonGroupBackground dark:focus-visible:border-l-darkButtonGroupBorder'
|
|
11
|
+
: buttonsStyles[button] === 'rounded-left-with-right-border' ? 'border rounded-s-default' : '',
|
|
12
|
+
(button === activeButton || props.solidColor === true) ? 'bg-lightButtonGroupActiveBackground text-lightButtonGroupActiveText focus-visible:ring-lightButtonGroupActiveFocusRing dark:bg-darkButtonGroupActiveBackground dark:text-darkButtonGroupActiveText dark:focus-visible:ring-darkButtonGroupActiveFocusRing'
|
|
13
|
+
:'text-lightButtonGroupText bg-lightButtonGroupBackground focus-visible:ring-lightButtonGroupFocusRing hover:bg-lightButtonGroupBackgroundHover hover:text-lightButtonGroupTextHover dark:bg-darkButtonGroupBackground dark:text-darkButtonGroupText dark:hover:text-darkButtonGroupTextHover dark:hover:bg-darkButtonGroupBackgroundHover dark:focus-visible:ring-darkButtonGroupFocusRing'
|
|
14
14
|
]"
|
|
15
15
|
@click="setActiveButton(button)"
|
|
16
16
|
>
|
|
@@ -1,25 +1,46 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
]"
|
|
8
|
-
>
|
|
9
|
-
|
|
10
|
-
<h5 class="font-bold tracking-tight text-lightCardTitle dark:text-darkCardTitle" :class="[props.size === 'sm' ? 'text-base' : props.size === 'md' ? 'text-lg mb-1' : 'text-xl mb-2']">{{ props.title }}</h5>
|
|
11
|
-
<p class="font-normal text-lightCardDescription dark:text-darkCardDescription" :class="[props.size === 'sm' ? 'text-sm' : props.size === 'md' ? 'text-base' : 'text-lg']">{{ props.description }}</p>
|
|
2
|
+
<div v-bind="restAttrs" :class="rootClasses">
|
|
3
|
+
<div v-if="title || description" class="afcl-card-header" :class="{ 'mb-4': $slots.default }">
|
|
4
|
+
<h3 v-if="title" class="afcl-card-title font-semibold tracking-tight text-lightCardTitle dark:text-darkCardTitle" :class="TITLE_SIZES[size]">{{ title }}</h3>
|
|
5
|
+
<p v-if="description" class="afcl-card-description mt-1 text-lightCardDescription dark:text-darkCardDescription" :class="DESCRIPTION_SIZES[size]">{{ description }}</p>
|
|
6
|
+
</div>
|
|
12
7
|
<slot></slot>
|
|
13
|
-
</
|
|
8
|
+
</div>
|
|
14
9
|
</template>
|
|
15
10
|
|
|
16
11
|
|
|
17
12
|
<script setup lang="ts">
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
import { computed, useAttrs } from 'vue';
|
|
14
|
+
import { type ClassNameValue } from 'tailwind-merge';
|
|
15
|
+
import { mergeClasses } from './classMerge';
|
|
16
|
+
|
|
17
|
+
type CardSize = 'sm' | 'md' | 'lg';
|
|
18
|
+
|
|
19
|
+
defineOptions({ inheritAttrs: false });
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<{
|
|
22
|
+
title?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
size?: CardSize;
|
|
25
|
+
hideBorder?: boolean;
|
|
26
|
+
}>(), {
|
|
27
|
+
size: 'lg',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const PADDINGS: Record<CardSize, string> = { sm: 'p-3', md: 'p-4', lg: 'p-6' };
|
|
31
|
+
const TITLE_SIZES: Record<CardSize, string> = { sm: 'text-sm', md: 'text-base', lg: 'text-lg' };
|
|
32
|
+
const DESCRIPTION_SIZES: Record<CardSize, string> = { sm: 'text-xs', md: 'text-sm', lg: 'text-base' };
|
|
33
|
+
|
|
34
|
+
const attrs = useAttrs();
|
|
35
|
+
const restAttrs = computed(() => {
|
|
36
|
+
const { class: _class, ...rest } = attrs;
|
|
37
|
+
return rest;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const rootClasses = computed(() => mergeClasses(
|
|
41
|
+
'afcl-card rounded-lg bg-lightCardBackground dark:bg-darkCardBackground',
|
|
42
|
+
!props.hideBorder && 'border border-lightCardBorder dark:border-darkCardBorder',
|
|
43
|
+
PADDINGS[props.size],
|
|
44
|
+
attrs.class as ClassNameValue,
|
|
45
|
+
));
|
|
46
|
+
</script>
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
:checked="props.modelValue"
|
|
8
8
|
:disabled="props.disabled"
|
|
9
9
|
@change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
|
|
10
|
-
class="peer appearance-none min-w-4 min-h-4 bg-lightCheckboxBgUnchecked border border-lightCheckboxBorderColor rounded-
|
|
11
|
-
focus:
|
|
12
|
-
focus:ring-
|
|
10
|
+
class="peer appearance-none min-w-4 min-h-4 bg-lightCheckboxBgUnchecked border border-lightCheckboxBorderColor rounded shadow-customLight checked:bg-lightCheckboxBgChecked checked:border-lightCheckboxBgChecked
|
|
11
|
+
focus:outline-none focus-visible:ring-lightFocusRing dark:focus-visible:ring-darkFocusRing
|
|
12
|
+
focus-visible:ring-[3px] focus-visible:ring-offset-0 dark:bg-darkCheckboxBgUnchecked dark:border-darkCheckboxBorderColor dark:checked:bg-darkCheckboxBgChecked dark:checked:border-darkCheckboxBgChecked cursor-pointer transition-colors"
|
|
13
13
|
>
|
|
14
14
|
<div class="pointer-events-none absolute text-lightCheckboxIconColor dark:text-darkCheckboxIconColor leading-none peer-checked:block hidden">
|
|
15
|
-
<
|
|
15
|
+
<IconCheck class="w-3 h-3 [&_g]:[stroke-width:3]" />
|
|
16
16
|
</div>
|
|
17
17
|
</div>
|
|
18
|
-
<label :for="id" class="ms-2 text-sm
|
|
18
|
+
<label :for="id" class="ms-2 text-sm text-lightTextLabel dark:text-darkTextLabel">
|
|
19
19
|
<slot></slot>
|
|
20
20
|
</label>
|
|
21
21
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
</template>
|
|
24
24
|
|
|
25
25
|
<script setup lang="ts">
|
|
26
|
-
import {
|
|
26
|
+
import { IconCheck } from '@iconify-prerendered/vue-lucide';
|
|
27
27
|
const props = defineProps({
|
|
28
28
|
modelValue: Boolean,
|
|
29
29
|
disabled: Boolean,
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="aspect-[4/3] w-8 h-6 flex items-center justify-center">
|
|
3
|
-
<component v-if="
|
|
4
|
-
<span v-else-if="countryCode">{{ countryCode }}</span>
|
|
3
|
+
<component v-if="flagComponent" :is="flagComponent" class="flag-icon h-full object-contain rounded-sm"/>
|
|
4
|
+
<span v-else-if="flagIcons && countryCode">{{ countryCode }}</span>
|
|
5
5
|
</div>
|
|
6
6
|
</template>
|
|
7
7
|
|
|
8
8
|
<script setup lang="ts">
|
|
9
|
-
import
|
|
9
|
+
import { computed, shallowRef, type Component } from 'vue';
|
|
10
10
|
|
|
11
|
-
defineProps(['countryCode']);
|
|
11
|
+
const props = defineProps(['countryCode']);
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
// the flag set takes megabytes, so it is loaded when a flag is shown for the first time
|
|
14
|
+
const flagIcons = shallowRef<Record<string, Component>>();
|
|
15
|
+
import('@iconify-prerendered/vue-flag').then((icons) => {
|
|
16
|
+
flagIcons.value = icons;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const flagComponent = computed(() => {
|
|
20
|
+
if (!props.countryCode || !flagIcons.value) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const normalizedCode = props.countryCode.charAt(0).toUpperCase() + props.countryCode.slice(1).toLowerCase();
|
|
24
|
+
return flagIcons.value[`Icon${normalizedCode}4x3`] || null; // 4:3 aspect ratio flags
|
|
25
|
+
});
|
|
20
26
|
</script>
|
|
21
27
|
|
|
22
28
|
<style scoped>
|