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
@@ -1,15 +1,15 @@
1
1
  <template>
2
- <div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl border dark:border-darkFormBorder">
3
- <div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-darkFormBorder text-lightListTableHeadingText bg-lightFormHeading dark:bg-darkFormHeading dark:text-darkListTableHeadingText rounded-t-lg">
2
+ <div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow border border-lightFormBorder bg-lightForm dark:bg-darkForm dark:border-darkFormBorder">
3
+ <div v-if="group.groupName && !group.noTitle" class="text-sm font-semibold px-5 py-3 flex flex-1 items-center border-b border-lightFormBorder dark:border-darkFormBorder text-lightFormFieldTextColor bg-lightFormHeading dark:bg-darkFormHeading dark:text-darkFormFieldTextColor rounded-t-lg">
4
4
  {{ group.groupName }}
5
5
  </div>
6
6
  <table class="w-full text-sm text-left rtl:text-right text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">
7
- <thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-lightListTableHeadingText uppercase dark:text-darkListTableHeadingText bg-lightFormHeading dark:bg-darkFormHeading block md:table-row-group ">
7
+ <thead v-if="!allColumnsHaveCustomComponent" class="text-xs font-medium text-lightListTablePaginationHelpText dark:text-darkListTablePaginationHelpText bg-lightFormHeading dark:bg-darkFormHeading block md:table-row-group border-b border-lightFormBorder dark:border-darkFormBorder">
8
8
  <tr>
9
- <th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-6 py-3 hidden md:w-52 md:table-cell">
9
+ <th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-5 py-2.5 font-medium hidden md:w-56 md:table-cell">
10
10
  {{ $t('Field') }}
11
11
  </th>
12
- <th scope="col" :class="{'rounded-tr-lg': !group.groupName}" class="px-6 py-3 hidden md:table-cell">
12
+ <th scope="col" :class="{'rounded-tr-lg': !group.groupName}" class="px-5 py-2.5 font-medium hidden md:table-cell">
13
13
  {{ $t('Value') }}
14
14
  </th>
15
15
  </tr>
@@ -19,17 +19,17 @@
19
19
  v-for="(column, i) in group.columns"
20
20
  :key="column.name"
21
21
  v-if="currentValues !== null"
22
- class="bg-lightForm dark:bg-darkForm dark:border-darkFormBorder block md:table-row"
22
+ class="bg-lightForm dark:bg-darkForm border-lightFormBorder dark:border-darkFormBorder block md:table-row"
23
23
  :class="{ 'border-b': i !== group.columns.length - 1}"
24
24
  >
25
- <td class="px-6 py-4 flex items-center block pb-0 md:pb-4 relative md:table-cell"
25
+ <td class="px-5 py-3 flex items-center block pb-0 md:pb-3 relative md:table-cell"
26
26
  :class="{'rounded-bl-lg border-b-none': i === group.columns.length - 1}"> <!--align-top-->
27
- <div class="absolute inset-0 flex items-center overflow-hidden px-6 py-4 max-h-32">
28
- <span class="flex items-center gap-1">
27
+ <div class="absolute inset-0 flex items-center overflow-hidden px-5 py-3 max-h-32">
28
+ <span class="flex items-center gap-1 font-medium">
29
29
  {{ column.label }}
30
30
  <Tooltip v-if="column.required[mode]">
31
31
 
32
- <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
32
+ <IconAsterisk v-if="column.required[mode]" class="w-3 h-3"
33
33
  :class="(columnsWithErrors[column.name] && validatingMode && !isValidating) ? 'text-lightInputErrorColor dark:text-darkInputErrorColor' : 'text-lightRequiredIconColor dark:text-darkRequiredIconColor'"
34
34
  />
35
35
 
@@ -41,7 +41,7 @@
41
41
  </div>
42
42
  </td>
43
43
  <td
44
- class="px-6 py-4 whitespace-pre-wrap relative block md:table-cell"
44
+ class="px-5 py-3 whitespace-pre-wrap relative block md:table-cell"
45
45
  :class="{'rounded-br-lg': i === group.columns.length - 1}"
46
46
  >
47
47
  <ColumnValueInputWrapper
@@ -68,7 +68,7 @@
68
68
  </template>
69
69
 
70
70
  <script setup lang="ts">
71
- import { IconExclamationCircleSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
71
+ import { IconAsterisk } from '@iconify-prerendered/vue-lucide';
72
72
  import { Tooltip } from '@/afcl';
73
73
  import { ref, computed, watch, nextTick, type Ref } from 'vue';
74
74
  import { useI18n } from 'vue-i18n';
@@ -1,115 +1,93 @@
1
1
  <template>
2
- <div class="relative inline-block">
3
- <div
4
- ref="triggerRef"
5
- class="border border-gray-300 dark:border-gray-700 dark:border-opacity-0 border-opacity-0 hover:border-opacity-100 dark:hover:border-opacity-100 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
6
- @click="toggleMenu"
2
+ <DropdownMenu
3
+ v-model:open="showMenu"
4
+ placement="bottom-end"
5
+ teleportToBody
6
+ :contentClass="['af-list-actions-three-dots z-40 min-w-[10rem]', THREE_DOTS_MENU_CONTENT_CLASSES]"
7
+ >
8
+ <template #trigger>
9
+ <Button variant="ghost" size="sm" iconOnly>
10
+ <IconEllipsis />
11
+ </Button>
12
+ </template>
13
+
14
+ <DropdownMenuItem
15
+ v-if="isBaseActionInMenu('show')"
16
+ :class="THREE_DOTS_MENU_ITEM_CLASSES"
17
+ :to="{ name: 'resource-show', params: { resourceId: props.resourceId, primaryKey: record._primaryKeyValue } }"
18
+ >
19
+ <template #icon><IconEye /></template>
20
+ {{ $t('Show item') }}
21
+ </DropdownMenuItem>
22
+ <DropdownMenuItem
23
+ v-if="isBaseActionInMenu('edit')"
24
+ :class="THREE_DOTS_MENU_ITEM_CLASSES"
25
+ :to="{ name: 'resource-edit', params: { resourceId: props.resourceId, primaryKey: record._primaryKeyValue } }"
26
+ >
27
+ <template #icon><IconPencil /></template>
28
+ {{ $t('Edit item') }}
29
+ </DropdownMenuItem>
30
+ <DropdownMenuItem
31
+ v-if="isBaseActionInMenu('delete')"
32
+ :class="THREE_DOTS_MENU_ITEM_CLASSES"
33
+ @click="deleteRecord(record)"
34
+ >
35
+ <template #icon><IconTrash2 /></template>
36
+ {{ $t('Delete item') }}
37
+ </DropdownMenuItem>
38
+
39
+ <DropdownMenuItem
40
+ v-for="action in menuActions"
41
+ :key="action.id"
42
+ :class="THREE_DOTS_MENU_ITEM_CLASSES"
43
+ @click="actionItemClick(action.id, $event)"
7
44
  >
8
- <IconDotsHorizontalOutline class="w-6 h-6 text-lightPrimary dark:text-darkPrimary dark:brightness-150" />
9
- </div>
10
- <teleport to="body">
11
- <div
12
- v-if="showMenu"
13
- ref="menuRef"
14
- class="af-list-actions-three-dots z-40 bg-white dark:bg-gray-900 rounded-md shadow-lg border dark:border-gray-700 py-1"
15
- :style="menuStyles"
45
+ <component
46
+ :ref="(el: any) => { actionRefs[action.id] = el; }"
47
+ :is="action.customComponent ? getCustomComponent(formatComponent(action.customComponent)) : CallActionWrapper"
48
+ :meta="formatComponent(action.customComponent).meta"
49
+ :row="record"
50
+ :resource="coreStore.resource"
51
+ :adminUser="coreStore.adminUser"
52
+ @callAction="(payload? : Object) => startCustomAction(action.id, record, payload)"
16
53
  >
17
- <template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('show'))">
18
- <RouterLink
19
- v-if="resourceOptions?.allowedActions?.show"
20
- class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
21
- :to="{
22
- name: 'resource-show',
23
- params: {
24
- resourceId: props.resourceId,
25
- primaryKey: record._primaryKeyValue,
26
- }
27
- }"
54
+ <!-- preventDefault marks the click as handled by the action component -->
55
+ <span @click.prevent class="flex flex-1 items-center gap-2">
56
+ <component
57
+ v-if="action.icon"
58
+ :is="getIcon(action.icon)"
59
+ class="w-4 h-4 text-lightInputIconColor dark:text-darkInputIconColor"
60
+ />
61
+ {{ $t(action.name) }}
62
+ </span>
63
+ </component>
64
+ </DropdownMenuItem>
28
65
 
29
- >
30
- <IconEyeSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
31
- {{ $t('Show item') }}
32
- </RouterLink>
33
- </template>
34
- <template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('edit'))">
35
- <RouterLink
36
- v-if="resourceOptions?.allowedActions?.edit"
37
- class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
38
- :to="{
39
- name: 'resource-edit',
40
- params: {
41
- resourceId: props.resourceId,
42
- primaryKey: record._primaryKeyValue,
43
- }
44
- }"
45
- >
46
- <IconPenSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
47
- {{ $t('Edit item') }}
48
- </RouterLink>
49
- </template>
50
- <template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('delete'))">
51
- <button
52
- v-if="resourceOptions?.allowedActions?.delete"
53
- class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
54
- @click="deleteRecord(record)"
55
- >
56
- <IconTrashBinSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
57
- {{ $t('Delete item') }}
58
- </button>
59
- </template>
60
- <div v-for="action in (resourceOptions.actions ?? []).filter((a: AdminForthActionInput) => a.showIn?.listThreeDotsMenu)" :key="action.id" >
61
- <button class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300" @click="() => { startCustomAction(action.id, record); showMenu = false; }">
62
- <component
63
- :is="action.customComponent ? getCustomComponent(action.customComponent) : CallActionWrapper"
64
- :meta="action.customComponent?.meta"
65
- :row="record"
66
- :resource="coreStore.resource"
67
- :adminUser="coreStore.adminUser"
68
- @callAction="(payload? : Object) => startCustomAction(action.id, record, payload)"
69
- >
70
- <component
71
- v-if="action.icon"
72
- :is="getIcon(action.icon)"
73
- class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary dark:brightness-200"
74
- />
75
- {{ $t(action.name) }}
76
- </component>
77
- </button>
78
- </div>
79
- <template v-if="customActionIconsThreeDotsMenuItems">
80
- <component
81
- v-for="c in customActionIconsThreeDotsMenuItems"
82
- :is="getCustomComponent(formatComponent(c))"
83
- :meta="formatComponent(c).meta"
84
- :resource="coreStore.resource"
85
- :adminUser="coreStore.adminUser"
86
- :record="record"
87
- :updateRecords="props.updateRecords"
88
- />
89
- </template>
90
- </div>
91
- </teleport>
92
- </div>
66
+ <component
67
+ v-for="c in customActionIconsThreeDotsMenuItems"
68
+ :is="getCustomComponent(formatComponent(c))"
69
+ :meta="formatComponent(c).meta"
70
+ :resource="coreStore.resource"
71
+ :adminUser="coreStore.adminUser"
72
+ :record="record"
73
+ :updateRecords="props.updateRecords"
74
+ />
75
+ </DropdownMenu>
93
76
  </template>
94
77
 
95
78
  <script lang="ts" setup>
96
- import {
97
- IconEyeSolid,
98
- IconPenSolid,
99
- IconTrashBinSolid,
100
- IconDotsHorizontalOutline
101
- } from '@iconify-prerendered/vue-flowbite';
102
- import { onMounted, onBeforeUnmount, ref, nextTick, watch } from 'vue';
79
+ import { IconEye, IconPencil, IconTrash2, IconEllipsis } from '@iconify-prerendered/vue-lucide';
80
+ import { computed, ref } from 'vue';
103
81
  import { getIcon, getCustomComponent, formatComponent } from '@/utils';
82
+ import { THREE_DOTS_MENU_CONTENT_CLASSES, THREE_DOTS_MENU_ITEM_CLASSES } from '@/utils/threeDotsMenu';
104
83
  import { useCoreStore } from '@/stores/core';
84
+ import { Button, DropdownMenu, DropdownMenuItem } from '@/afcl';
105
85
  import CallActionWrapper from '@/components/CallActionWrapper.vue'
106
- import { type AdminForthActionInput, type AdminForthComponentDeclaration, type AdminForthComponentDeclarationFull } from '@/types/Common';
86
+ import { type AdminForthActionFront, type AdminForthComponentDeclaration } from '@/types/Common';
107
87
 
108
88
  const coreStore = useCoreStore();
109
89
  const showMenu = ref(false);
110
- const triggerRef = ref<HTMLElement | null>(null);
111
- const menuRef = ref<HTMLElement | null>(null);
112
- const menuStyles = ref<Record<string, string>>({});
90
+ const actionRefs: Record<string, any> = {};
113
91
 
114
92
  const props = defineProps<{
115
93
  resourceOptions: any;
@@ -121,116 +99,20 @@ const props = defineProps<{
121
99
  startCustomAction: (actionId: string, row: any, extraData?: Record<string, any>) => void;
122
100
  }>();
123
101
 
124
- onMounted(() => {
125
- window.addEventListener('scroll', handleScrollOrResize, true);
126
- window.addEventListener('resize', handleScrollOrResize);
127
- document.addEventListener('click', handleOutsideClick, true);
128
- });
102
+ const menuActions = computed<AdminForthActionFront[]>(
103
+ () => (props.resourceOptions.actions ?? []).filter((a: AdminForthActionFront) => a.showIn?.listThreeDotsMenu)
104
+ );
129
105
 
130
- onBeforeUnmount(() => {
131
- window.removeEventListener('scroll', handleScrollOrResize, true);
132
- window.removeEventListener('resize', handleScrollOrResize);
133
- document.removeEventListener('click', handleOutsideClick, true);
134
- });
135
-
136
- watch(showMenu, async (isOpen) => {
137
- if (isOpen) {
138
- await nextTick();
139
- // First pass: after DOM mount
140
- updateMenuPosition();
141
- // Second pass: after layout/paint to catch width changes (fonts/icons)
142
- requestAnimationFrame(() => {
143
- updateMenuPosition();
144
- // Final safety: one micro-delay retry if width was still 0
145
- setTimeout(() => updateMenuPosition(), 0);
146
- });
147
- }
148
- });
149
-
150
- function toggleMenu() {
151
- if (!showMenu.value) {
152
- // Provisional position to avoid flashing at left:0 on first open
153
- const el = triggerRef.value;
154
- if (el) {
155
- const rect = el.getBoundingClientRect();
156
- const gap = 8;
157
- menuStyles.value = {
158
- position: 'fixed',
159
- top: `${Math.round(rect.bottom)}px`,
160
- left: `${Math.round(rect.left)}px`,
161
- };
162
- }
163
- }
164
- showMenu.value = !showMenu.value;
106
+ // a base action is listed here unless it is rendered as a quick icon in the row
107
+ function isBaseActionInMenu(action: 'show' | 'edit' | 'delete') {
108
+ return props.resourceOptions?.allowedActions?.[action] && !props.resourceOptions?.baseActionsAsQuickIcons?.includes(action);
165
109
  }
166
110
 
167
- function updateMenuPosition() {
168
- const el = triggerRef.value;
169
- if (!el) return;
170
- const rect = el.getBoundingClientRect();
171
- const margin = 8; // gap around the trigger/menu
172
- const menuEl = menuRef.value;
173
- // Measure current menu size to align and decide flipping
174
- let menuWidth = rect.width; // fallback to trigger width
175
- let menuHeight = 0;
176
- if (menuEl) {
177
- const menuRect = menuEl.getBoundingClientRect();
178
- // Prefer bounding rect; fallback to offset/scroll width if needed
179
- const measuredW = menuRect.width || menuEl.offsetWidth || menuEl.scrollWidth;
180
- if (measuredW > 0) menuWidth = measuredW;
181
- const measuredH = menuRect.height || menuEl.offsetHeight || menuEl.scrollHeight;
182
- if (measuredH > 0) menuHeight = measuredH;
111
+ // a click on the item padding is forwarded to the action component (see `click` expose of custom action components),
112
+ // clicks on its content are already handled by the component itself
113
+ function actionItemClick(actionId: string, event: MouseEvent) {
114
+ if (!event.defaultPrevented) {
115
+ actionRefs[actionId]?.click?.();
183
116
  }
184
- // Right-align: right edge of menu == right edge of trigger
185
- let left = rect.right - menuWidth;
186
- // Clamp within viewport with small margin so it doesn't render off-screen
187
- const minLeft = margin;
188
- const maxLeft = Math.max(minLeft, window.innerWidth - margin - menuWidth);
189
- left = Math.min(Math.max(left, minLeft), maxLeft);
190
-
191
- // Determine whether to place above or below based on available space
192
- const spaceBelow = window.innerHeight - rect.bottom - margin;
193
- const spaceAbove = rect.top - margin;
194
- const maxMenuHeight = Math.max(0, window.innerHeight - 2 * margin);
195
-
196
- let top: number;
197
- if (menuHeight === 0) {
198
- // Unknown height yet (first pass). Prefer placing below; a subsequent pass will correct if needed.
199
- top = rect.bottom + margin;
200
- } else if (menuHeight <= spaceBelow) {
201
- // Enough space below
202
- top = rect.bottom + margin;
203
- } else if (menuHeight <= spaceAbove) {
204
- // Not enough below but enough above -> flip
205
- top = rect.top - margin - menuHeight;
206
- } else {
207
- // Not enough space on either side: pick the side with more room and clamp within viewport
208
- if (spaceBelow >= spaceAbove) {
209
- top = Math.min(rect.bottom + margin, window.innerHeight - margin - menuHeight);
210
- } else {
211
- top = Math.max(margin, rect.top - margin - menuHeight);
212
- }
213
- }
214
-
215
- menuStyles.value = {
216
- position: 'fixed',
217
- top: `${Math.round(top)}px`,
218
- left: `${Math.round(left)}px`,
219
- maxHeight: `${Math.round(maxMenuHeight)}px`,
220
- overflowY: 'auto',
221
- };
222
117
  }
223
-
224
- function handleScrollOrResize() {
225
- showMenu.value = false;
226
- }
227
-
228
- function handleOutsideClick(e: MouseEvent) {
229
- const target = e.target as Node | null;
230
- if (!target) return;
231
- if (menuRef.value?.contains(target)) return;
232
- if (triggerRef.value?.contains(target)) return;
233
- showMenu.value = false;
234
- }
235
-
236
- </script>
118
+ </script>
@@ -5,55 +5,58 @@
5
5
  ? { href: item.url }
6
6
  : { to: item.url || { name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {} } }"
7
7
  :target="item.isOpenInNewTab ? '_blank' : '_self'"
8
- class="af-menu-link flex group relative items-center py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default transition-all duration-200 ease-in-out"
8
+ class="af-menu-link flex group relative items-center py-1.5 text-lightSidebarText dark:text-darkSidebarText rounded-default transition-colors duration-150 ease-in-out"
9
9
  :class="{
10
- '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),
11
- 'pl-6 pr-3.5': (isChild && !isSidebarIconOnly && !isSidebarHovering) || (isChild && isSidebarIconOnly && isSidebarHovering),
10
+ 'hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarItemActive dark:active:bg-darkSidebarItemActive': !['divider', 'gap', 'heading'].includes(item.type),
11
+ 'pl-2.5 pr-3': isChild && !isSidebarIconOnly && !isSidebarHovering,
12
+ 'pl-6 pr-3': isChild && isSidebarIconOnly && isSidebarHovering,
12
13
  'w-full': !isSidebarIconOnly || isSidebarHovering,
13
- 'px-3.5': !isChild && (!isSidebarIconOnly || isSidebarHovering),
14
- 'w-10 h-10 ml-1 px-2.5': isSidebarIconOnly && !isSidebarHovering,
15
- 'bg-lightSidebarItemActive dark:bg-darkSidebarItemActive': isItemActive(item)
14
+ 'px-3': !isChild && (!isSidebarIconOnly || isSidebarHovering),
15
+ 'w-10 h-9 ml-1 px-3': isSidebarIconOnly && !isSidebarHovering,
16
+ 'bg-lightSidebarItemActive text-lightSidebarTextActive font-medium dark:bg-darkSidebarItemActive dark:text-darkSidebarTextActive': isItemActive(item)
16
17
  }"
17
18
  >
18
- <component v-if="item.icon" :is="getIcon(item.icon)"
19
- 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"
19
+ <component v-if="item.icon" :is="getIcon(item.icon)"
20
+ class="min-w-4 w-4 h-4 text-lightSidebarIcons dark:text-darkSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover transition-colors duration-150 ease-in-out"
21
+ :class="{ 'text-lightSidebarIconsHover dark:text-darkSidebarIconsHover': isItemActive(item) }"
20
22
  >
21
23
  </component>
22
- <IconFileImageOutline v-else
23
- 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"
24
+ <IconFileImage v-else-if="!isChild || isSidebarIconOnly"
25
+ class="min-w-4 w-4 h-4 text-lightSidebarIcons dark:text-darkSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover transition-colors duration-150 ease-in-out"
24
26
  />
25
27
  <div
26
28
  class="overflow-hidden block ms-3 pr-4 text-left rtl:text-right transition-all duration-200 ease-in-out"
27
29
  :class="{
28
30
  'opacity-0 ms-0 translate-x-4 flex-none': isSidebarIconOnly && !isSidebarHovering,
29
31
  'opacity-100 ms-3 translate-x-0 flex-1': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering),
32
+ '!ms-0': isChild && !item.icon && !isSidebarIconOnly,
30
33
  }"
31
34
  :style="isSidebarIconOnly ? {
32
35
  minWidth: isChild
33
- ? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)`
34
- : `calc(${expandedWidth} - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)`,
36
+ ? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 - 1rem - 0.75rem)`
37
+ : `calc(${expandedWidth} - 0.75rem*2 - 0.75rem*2 - 1rem - 0.75rem)`,
35
38
  width: isChild
36
- ? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)`
37
- : `calc(${expandedWidth} - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)`
39
+ ? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 - 1rem - 0.75rem)`
40
+ : `calc(${expandedWidth} - 0.75rem*2 - 0.75rem*2 - 1rem - 0.75rem)`
38
41
  } : {}"
39
42
  >
40
43
  {{ item.label }}
41
44
  </div>
42
45
  <span class="absolute flex items-center justify-center right-1 top-1/2 -translate-y-1/2" v-if="(item.badge || item.badge === 0) && showExpandedBadge ">
43
46
  <Tooltip v-if="item.badgeTooltip">
44
- <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
45
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
47
+ <div class="af-badge inline-flex items-center justify-center h-5 px-1.5 ms-2 text-[11px] font-medium tabular-nums rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
48
+ fill-lightAnnouncementText dark:fill-darkAnnouncementText text-lightAnnouncementText dark:text-darkAnnouncementText min-w-[1.25rem] max-w-[3rem]">{{ item.badge }}</div>
46
49
  <template #tooltip>
47
50
  {{ item.badgeTooltip }}
48
51
  </template>
49
52
  </Tooltip>
50
53
  <template v-else>
51
- <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
52
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
54
+ <div class="af-badge inline-flex items-center justify-center h-5 px-1.5 ms-2 text-[11px] font-medium tabular-nums rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
55
+ fill-lightAnnouncementText dark:fill-darkAnnouncementText text-lightAnnouncementText dark:text-darkAnnouncementText min-w-[1.25rem] max-w-[3rem]">{{ item.badge }}</div>
53
56
  </template>
54
57
  </span>
55
58
  <div v-if="(item.badge || item.badge === 0) && 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
56
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
59
+ fill-lightAnnouncementText dark:fill-darkAnnouncementText text-lightAnnouncementText dark:text-darkAnnouncementText">
57
60
  </div>
58
61
  </component>
59
62
  </template>
@@ -63,7 +66,7 @@ import { getIcon } from '@/utils';
63
66
  import { Tooltip } from '@/afcl';
64
67
  import { ref, watch, computed } from 'vue';
65
68
  import { useCoreStore } from '@/stores/core';
66
- import { IconFileImageOutline } from '@iconify-prerendered/vue-flowbite';
69
+ import { IconFileImage } from '@iconify-prerendered/vue-lucide';
67
70
  import { useRoute, RouterLink } from 'vue-router';
68
71
 
69
72
  const isExternalUrl = (url: string ) => /^https?:\/\//.test(url || '');
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <div class="range-slider" ref="trackRef" @mousedown="onTrackMouseDown">
3
- <div class="track"></div>
4
- <div class="range bg-lightPrimary/30" :style="rangeStyle"></div>
3
+ <div class="track bg-lightProgressBarUnfilledColor dark:bg-darkProgressBarUnfilledColor"></div>
4
+ <div class="range bg-lightPrimary dark:bg-darkPrimary" :style="rangeStyle"></div>
5
5
 
6
6
  <div
7
- class="bg-lightPrimary thumb"
7
+ class="thumb bg-white border border-lightInputBorderHover shadow-[0_1px_2px_rgba(24,24,27,0.2)] dark:border-darkInputBorder"
8
8
  :style="minThumbStyle"
9
9
  @mousedown.stop.prevent="startDrag('min', $event)"
10
10
  @mouseenter="minHovered = true"
@@ -13,7 +13,7 @@
13
13
  <div v-if="minHovered || activeThumb === 'min'" class="thumb-tooltip" :style="minTooltipStyle">{{ minVal }}</div>
14
14
 
15
15
  <div
16
- class="bg-lightPrimary thumb"
16
+ class="thumb bg-white border border-lightInputBorderHover shadow-[0_1px_2px_rgba(24,24,27,0.2)] dark:border-darkInputBorder"
17
17
  :style="maxThumbStyle"
18
18
  @mousedown.stop.prevent="startDrag('max', $event)"
19
19
  @mouseenter="maxHovered = true"
@@ -34,8 +34,8 @@ const props = defineProps({
34
34
  },
35
35
  min: { type: Number, default: 0 },
36
36
  max: { type: Number, default: 100 },
37
- dotSize: { type: Number, default: 20 },
38
- height: { type: String, default: '8px' }
37
+ dotSize: { type: Number, default: 16 },
38
+ height: { type: String, default: '4px' }
39
39
  })
40
40
 
41
41
  const emit = defineEmits(['update:modelValue'])
@@ -184,14 +184,13 @@ onBeforeUnmount(() => {
184
184
  .track {
185
185
  position: absolute;
186
186
  width: 100%;
187
- height: 8px;
188
- background: #e5e7eb;
187
+ height: 4px;
189
188
  border-radius: 9999px;
190
189
  }
191
190
 
192
191
  .range {
193
192
  position: absolute;
194
- height: 8px;
193
+ height: 4px;
195
194
  border-radius: 9999px;
196
195
  }
197
196
 
@@ -209,7 +208,7 @@ onBeforeUnmount(() => {
209
208
  transform: translateX(-50%);
210
209
  background: rgba(0, 0, 0, 0.75);
211
210
  color: #fff;
212
- font-size: 14px;
211
+ font-size: 12px;
213
212
  font-weight: 500;
214
213
  line-height: 1;
215
214
  padding: 6px 6px;