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.
Files changed (94) hide show
  1. package/dist/modules/codeInjector.d.ts.map +1 -1
  2. package/dist/modules/codeInjector.js +20 -3
  3. package/dist/modules/codeInjector.js.map +1 -1
  4. package/dist/modules/styleGenerator.d.ts +13 -0
  5. package/dist/modules/styleGenerator.d.ts.map +1 -1
  6. package/dist/modules/styleGenerator.js +274 -1
  7. package/dist/modules/styleGenerator.js.map +1 -1
  8. package/dist/modules/styles.d.ts +24 -0
  9. package/dist/modules/styles.d.ts.map +1 -1
  10. package/dist/modules/styles.js +452 -427
  11. package/dist/modules/styles.js.map +1 -1
  12. package/dist/modules/utils.d.ts +1 -13
  13. package/dist/modules/utils.d.ts.map +1 -1
  14. package/dist/modules/utils.js +2 -275
  15. package/dist/modules/utils.js.map +1 -1
  16. package/dist/spa/index.html +2 -2
  17. package/dist/spa/package-lock.json +1373 -1136
  18. package/dist/spa/package.json +18 -15
  19. package/dist/spa/pnpm-lock.yaml +1067 -648
  20. package/dist/spa/src/App.vue +84 -92
  21. package/dist/spa/src/afcl/AreaChart.vue +8 -24
  22. package/dist/spa/src/afcl/BarChart.vue +8 -23
  23. package/dist/spa/src/afcl/Button.vue +8 -2
  24. package/dist/spa/src/afcl/ButtonGroup.vue +9 -9
  25. package/dist/spa/src/afcl/Card.vue +40 -19
  26. package/dist/spa/src/afcl/Checkbox.vue +6 -6
  27. package/dist/spa/src/afcl/CountryFlag.vue +17 -11
  28. package/dist/spa/src/afcl/DatePicker.vue +7 -7
  29. package/dist/spa/src/afcl/Dialog.vue +8 -9
  30. package/dist/spa/src/afcl/DropdownMenu.vue +210 -0
  31. package/dist/spa/src/afcl/DropdownMenuAccordion.vue +56 -0
  32. package/dist/spa/src/afcl/DropdownMenuItem.vue +86 -0
  33. package/dist/spa/src/afcl/Dropzone.vue +10 -26
  34. package/dist/spa/src/afcl/Input.vue +4 -4
  35. package/dist/spa/src/afcl/Link.vue +1 -1
  36. package/dist/spa/src/afcl/LinkButton.vue +15 -1
  37. package/dist/spa/src/afcl/MixedChart.vue +4 -18
  38. package/dist/spa/src/afcl/Modal.vue +9 -9
  39. package/dist/spa/src/afcl/PieChart.vue +8 -29
  40. package/dist/spa/src/afcl/ProgressBar.vue +2 -2
  41. package/dist/spa/src/afcl/Select.vue +20 -20
  42. package/dist/spa/src/afcl/Skeleton.vue +1 -1
  43. package/dist/spa/src/afcl/Spinner.vue +1 -1
  44. package/dist/spa/src/afcl/Table.vue +19 -26
  45. package/dist/spa/src/afcl/Textarea.vue +1 -1
  46. package/dist/spa/src/afcl/Toggle.vue +4 -4
  47. package/dist/spa/src/afcl/Tooltip.vue +1 -1
  48. package/dist/spa/src/afcl/TreeMapChart.vue +5 -20
  49. package/dist/spa/src/afcl/VerticalTabs.vue +4 -4
  50. package/dist/spa/src/afcl/buttonStyles.ts +73 -33
  51. package/dist/spa/src/afcl/chartUtils.ts +19 -0
  52. package/dist/spa/src/afcl/classMerge.ts +11 -0
  53. package/dist/spa/src/afcl/dropdownMenuContext.ts +10 -0
  54. package/dist/spa/src/afcl/fileSize.ts +12 -0
  55. package/dist/spa/src/afcl/index.ts +3 -0
  56. package/dist/spa/src/components/AcceptModal.vue +17 -19
  57. package/dist/spa/src/components/Breadcrumbs.vue +11 -13
  58. package/dist/spa/src/components/BreadcrumbsWithButtons.vue +2 -2
  59. package/dist/spa/src/components/CallActionWrapper.vue +3 -0
  60. package/dist/spa/src/components/ColumnValueInput.vue +5 -5
  61. package/dist/spa/src/components/ColumnValueInputWrapper.vue +7 -6
  62. package/dist/spa/src/components/CustomDateRangePicker.vue +8 -8
  63. package/dist/spa/src/components/CustomRangePicker.vue +2 -2
  64. package/dist/spa/src/components/ErrorMessage.vue +3 -4
  65. package/dist/spa/src/components/Filters.vue +13 -13
  66. package/dist/spa/src/components/GroupsTable.vue +12 -12
  67. package/dist/spa/src/components/ListActionsThreeDots.vue +89 -207
  68. package/dist/spa/src/components/MenuLink.vue +23 -20
  69. package/dist/spa/src/components/RangePicker.vue +9 -10
  70. package/dist/spa/src/components/ResourceListTable.vue +68 -65
  71. package/dist/spa/src/components/ShowTable.vue +8 -8
  72. package/dist/spa/src/components/Sidebar.vue +31 -38
  73. package/dist/spa/src/components/ThreeDotsMenu.vue +88 -136
  74. package/dist/spa/src/components/Toast.vue +16 -23
  75. package/dist/spa/src/components/UserMenuSettingsButton.vue +15 -36
  76. package/dist/spa/src/components/ValueRenderer.vue +13 -8
  77. package/dist/spa/src/components/icons/IconCalendar.vue +3 -3
  78. package/dist/spa/src/components/icons/IconTime.vue +3 -3
  79. package/dist/spa/src/index.scss +6 -2
  80. package/dist/spa/src/main.ts +2 -0
  81. package/dist/spa/src/renderers/CompactField.vue +3 -3
  82. package/dist/spa/src/renderers/CompactUUID.vue +3 -3
  83. package/dist/spa/src/renderers/SensitiveBlurCell.vue +9 -9
  84. package/dist/spa/src/utils/threeDotsMenu.ts +7 -0
  85. package/dist/spa/src/utils/userMenu.ts +5 -0
  86. package/dist/spa/src/utils/utils.ts +12 -39
  87. package/dist/spa/src/views/CreateEditSkeleton.vue +3 -3
  88. package/dist/spa/src/views/CreateView.vue +5 -6
  89. package/dist/spa/src/views/EditView.vue +5 -6
  90. package/dist/spa/src/views/ListView.vue +114 -115
  91. package/dist/spa/src/views/LoginView.vue +4 -4
  92. package/dist/spa/src/views/ShowView.vue +17 -18
  93. package/dist/spa/vite.config.ts +23 -16
  94. package/package.json +1 -2
@@ -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 drop-shadow-sm bg-lightNavbar dark:bg-darkNavbar dark:border-darkSidebarDevider"
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 p-2 text-sm rounded-lg sm:hidden hover:bg-lightSidebarItemHover focus:outline-none focus:ring-2 focus:ring-lightSidebarDevider dark:text-darkSidebarIcons dark:hover:bg-darkSidebarHover dark:focus:ring-lightSidebarDevider">
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
- <svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
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-4">
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-8 h-8 text-red-500 hover:scale-110 transition-transform duration-200" />
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
- <span
32
+ <button
35
33
  v-if="!coreStore.config?.singleTheme"
36
- class="flex items-center gap-1 block py-2 text-sm text-black dark:text-darkSidebarTextHover dark:hover:text-darkSidebarTextActive" role="menuitem">
37
- <IconMoonSolid class="w-6 h-6 text-blue-300 hover:scale-110 cursor-pointer transition-transform duration-200" @click="toggleTheme" v-if="coreStore.theme !== 'dark'" />
38
- <IconSunSolid class="w-6 h-6 text-yellow-300 hover:scale-110 cursor-pointer transition-transform duration-200" @click="toggleTheme" v-else />
39
- </span>
40
-
41
- <div>
42
- <button
43
- ref="dropdownUserButton"
44
- type="button" class="flex text-sm bg- rounded-full focus:ring-4 focus:ring-lightSidebarDevider dark:focus:ring-darkSidebarDevider dark:bg-" aria-expanded="false">
45
- <span class="sr-only">{{ $t('Open user menu') }}</span>
46
- <!-- hover scale lives on the inner span: popper measures the button with transforms, so scaling it would shift the popup -->
47
- <span class="flex hover:scale-110 transition-transform duration-200">
48
- <img
49
- v-if="coreStore.userAvatarUrl"
50
- class="w-8 h-8 rounded-full object-cover"
51
- :src="coreStore.userAvatarUrl"
52
- alt="user photo"
53
- />
54
- <svg v-else class="w-8 h-8 text-lightNavbarIcons dark:text-darkNavbarIcons" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
55
- <path fill-rule="evenodd" d="M12 20a7.966 7.966 0 0 1-5.002-1.756l.002.001v-.683c0-1.794 1.492-3.25 3.333-3.25h3.334c1.84 0 3.333 1.456 3.333 3.25v.683A7.966 7.966 0 0 1 12 20ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10c0 5.5-4.44 9.963-9.932 10h-.138C6.438 21.962 2 17.5 2 12Zm10-5c-1.84 0-3.333 1.455-3.333 3.25S10.159 13.5 12 13.5c1.84 0 3.333-1.455 3.333-3.25S13.841 7 12 7Z" clip-rule="evenodd"/>
56
- </svg>
57
- </span>
58
- </button>
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
- </li>
79
- <li v-if="coreStore?.config?.settingPages && coreStore.config.settingPages.length > 0">
80
- <UserMenuSettingsButton />
81
- </li>
82
- <li>
83
- <button @click="logout" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover w-full" role="menuitem">{{ $t('Sign out') }}</button>
84
- </li>
85
- </ul>
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
- </Teleport>
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 dark:border-gray-700 mt-14">
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-gray-400 animate-bounce">
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-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-20">
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, nextTick, onMounted, onUnmounted, ref, watch, onBeforeMount } from 'vue';
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 { IconMoonSolid, IconSunSolid } from '@iconify-prerendered/vue-flowbite';
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 dropdownUserButton = ref(null);
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
- // before init flowbite we have to wait router initialized because it affects dom(our v-ifs) and fetch menu
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 { type ApexOptions } from 'apexcharts';
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: "Inter, sans-serif",
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.55,
50
+ opacityFrom: 0.18,
50
51
  opacityTo: 0,
51
- shade: "#1C64F2",
52
- gradientToColors: ["#1C64F2"],
52
+ shade: "#2a78d6",
53
+ gradientToColors: ["#2a78d6"],
53
54
  },
54
55
  },
55
56
  dataLabels: {
56
57
  enabled: false,
57
58
  },
58
59
  stroke: {
59
- width: 6,
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
- // for each of ...props.options merge on lowest level. so if { chart: {height : 2} }, it should not replace chart level, only height level
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 { type ApexOptions } from 'apexcharts';
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: string,
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: "Inter, sans-serif",
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: "Inter, sans-serif",
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
- // for each of ...props.options merge on lowest level. so if { chart: {height : 2} }, it should not replace chart level, only height level
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
- <slot></slot>
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-md shadow-xs" role="group">
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-2 dark:border-darkButtonGroupBorder disabled:opacity-50 disabled:cursor-not-allowed"
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-lg'
8
- : buttonsStyles[button] === 'rounded-left' ? 'border rounded-s-lg'
9
- : buttonsStyles[button] === 'rounded-right' ? 'border rounded-e-lg border-l-lightButtonGroupBackground focus:border-l-lightButtonGroupBorder dark:border-l-darkButtonGroupBackground dark:focus:border-l-darkButtonGroupBorder'
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-lg' : '',
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
- <a href="#"
3
- class="block max-w-sm bg-lightCardBackground border border-lightCardBorder rounded-lg shadow-sm hover:bg-lightCardBackgroundHover dark:bg-darkCardBackground dark:border-darkCardBorder dark:hover:bg-darkCardBackgroundHover"
4
- :class="[
5
- props.size === 'sm' ? 'p-2' : props.size === 'md' ? 'p-4' : 'p-6',
6
- props.hideBorder ? 'border-0' : ''
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
- </a>
8
+ </div>
14
9
  </template>
15
10
 
16
11
 
17
12
  <script setup lang="ts">
18
- const props = defineProps<{
19
- title?: string;
20
- description?: string;
21
- size?: 'sm' | 'md' | 'lg';
22
- hideBorder?: boolean;
23
- }>();
24
-
25
- </script>
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-sm checked:bg-lightCheckboxBgChecked
11
- focus:ring-lightFocusRing dark:focus:ring-darkFocusRing dark:focus:ring-darkFocusRing
12
- focus:ring-2 dark:bg-darkCheckboxBgUnchecked dark:border-darkCheckboxBorderColor dark:checked:bg-darkCheckboxBgChecked cursor-pointer"
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
- <IconCheckOutline class="w-[1.1rem] h-[1.1rem]" />
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 font-medium text-lightTextLabel dark:text-darkTextLabel">
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 { IconCheckOutline } from '@iconify-prerendered/vue-flowbite';
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="getFlagComponent(countryCode)" :is="getFlagComponent(countryCode)" class="flag-icon h-full object-contain rounded-sm"/>
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 * as FlagIcons from '@iconify-prerendered/vue-flag';
9
+ import { computed, shallowRef, type Component } from 'vue';
10
10
 
11
- defineProps(['countryCode']);
11
+ const props = defineProps(['countryCode']);
12
12
 
13
- const getFlagComponent = (countryCode: string) => {
14
- if (!countryCode) return null;
15
-
16
- const normalizedCode = countryCode.charAt(0).toUpperCase() + countryCode.slice(1).toLowerCase();
17
- const iconName = `Icon${normalizedCode}4x3`; // 4:3 aspect ratio flags
18
- return FlagIcons[iconName as keyof typeof FlagIcons] || null;
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>