adminforth 4.5.1 → 4.6.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.
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <div v-bind="restAttrs" :class="rootClasses">
3
+ <div :class="mergeClasses(
4
+ 'afcl-dialog-panel-header flex items-center gap-3.5 border-b border-lightDialogBreakLine py-5 pl-6 dark:border-darkDialogBreakLine',
5
+ closable ? 'pr-4' : 'pr-6',
6
+ classes.header,
7
+ )">
8
+ <div
9
+ v-if="$slots.icon"
10
+ :class="mergeClasses(
11
+ 'afcl-dialog-panel-icon flex w-10 h-10 shrink-0 items-center justify-center rounded-lg [&>svg]:w-5 [&>svg]:h-5',
12
+ 'bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary',
13
+ classes.icon,
14
+ )"
15
+ >
16
+ <slot name="icon" />
17
+ </div>
18
+ <div :class="mergeClasses('afcl-dialog-panel-title-wrap flex-1 min-w-0', classes.titleWrap)">
19
+ <slot name="title">
20
+ <h2 :class="mergeClasses('afcl-dialog-panel-title break-words text-lg leading-6 font-semibold text-lightDialogHeaderText dark:text-darkDialogHeaderText', classes.title)">{{ title }}</h2>
21
+ <p v-if="subtitle" :class="mergeClasses('afcl-dialog-panel-subtitle mt-0.5 text-sm leading-5 text-lightInputIconColor dark:text-darkInputIconColor', classes.subtitle)">{{ subtitle }}</p>
22
+ </slot>
23
+ </div>
24
+ <Button
25
+ v-if="closable"
26
+ type="button"
27
+ variant="ghost"
28
+ size="sm"
29
+ iconOnly
30
+ :aria-label="$t('Close')"
31
+ :class="mergeClasses(
32
+ 'afcl-dialog-panel-close text-lightDialogCloseButton hover:text-lightDialogCloseButtonHover hover:bg-lightDialogCloseButtonHoverBackground',
33
+ 'dark:text-darkDialogCloseButton dark:hover:text-darkDialogCloseButtonHover dark:hover:bg-darkDialogCloseButtonHoverBackground',
34
+ classes.close,
35
+ )"
36
+ @click="emit('close')"
37
+ >
38
+ <IconX aria-hidden="true" />
39
+ </Button>
40
+ </div>
41
+
42
+ <div :class="mergeClasses('afcl-dialog-panel-body flex flex-col gap-4 px-6 pt-5 pb-6', classes.body)">
43
+ <slot />
44
+ <p v-if="error" role="alert" :class="mergeClasses('afcl-dialog-panel-error -mt-1 flex items-center gap-1.5 text-sm text-lightDanger dark:text-darkDanger', classes.error)">
45
+ <IconCircleAlert class="w-4 h-4 shrink-0" aria-hidden="true" />
46
+ {{ error }}
47
+ </p>
48
+ </div>
49
+
50
+ <!-- a main action on the right, a secondary one may sit on the left with ms-0 me-auto -->
51
+ <div
52
+ v-if="$slots.footer"
53
+ :class="mergeClasses(
54
+ 'afcl-dialog-panel-footer flex flex-wrap items-center justify-end gap-x-3 gap-y-2 border-t border-lightDialogBreakLine bg-lightFormHeading py-3 pl-6 pr-5',
55
+ 'dark:border-darkDialogBreakLine dark:bg-darkFormHeading',
56
+ classes.footer,
57
+ )"
58
+ >
59
+ <slot name="footer" />
60
+ </div>
61
+ </div>
62
+ </template>
63
+
64
+ <script setup lang="ts">
65
+ import { computed, useAttrs } from 'vue';
66
+ import { type ClassNameValue } from 'tailwind-merge';
67
+ import { IconCircleAlert, IconX } from '@iconify-prerendered/vue-lucide';
68
+ import { mergeClasses } from './classMerge';
69
+ import Button from './Button.vue';
70
+
71
+ type DialogPanelPart = 'header' | 'icon' | 'titleWrap' | 'title' | 'subtitle' | 'close' | 'body' | 'error' | 'footer';
72
+
73
+ defineOptions({ inheritAttrs: false });
74
+
75
+ withDefaults(defineProps<{
76
+ title?: string,
77
+ subtitle?: string,
78
+ error?: string | null,
79
+ // shows a close button in the header which emits close
80
+ closable?: boolean,
81
+ // extra classes for the parts, e.g. the af-* classes apps restyle a screen by
82
+ classes?: Partial<Record<DialogPanelPart, ClassNameValue>>,
83
+ }>(), {
84
+ title: '',
85
+ subtitle: '',
86
+ error: null,
87
+ closable: false,
88
+ classes: () => ({}),
89
+ });
90
+
91
+ const emit = defineEmits<{
92
+ (e: 'close'): void,
93
+ }>();
94
+
95
+ const attrs = useAttrs();
96
+ const restAttrs = computed(() => {
97
+ const { class: _class, ...rest } = attrs;
98
+ return rest;
99
+ });
100
+
101
+ // a panel on the page is drawn like other surfaces: a hairline border and the light shadow of controls,
102
+ // a panel which floats over the page (in a modal) passes shadow-popover
103
+ const rootClasses = computed(() => mergeClasses(
104
+ 'afcl-dialog-panel relative flex flex-col w-full max-w-md overflow-hidden rounded-xl border shadow-customLight',
105
+ 'border-lightDialogBreakLine bg-lightDialogBackgorund dark:border-darkDialogBreakLine dark:bg-darkDialogBackgorund',
106
+ attrs.class as ClassNameValue,
107
+ ));
108
+ </script>
@@ -11,19 +11,39 @@
11
11
  <input
12
12
  ref="input"
13
13
  v-bind="$attrs"
14
- :type="type"
14
+ :type="passwordToggle && revealed ? 'text' : type"
15
15
  @input="$emit('update:modelValue', type === 'number' ? numberInputValue(($event.target as HTMLInputElement)?.value) : ($event.target as HTMLInputElement)?.value)"
16
16
  :value="modelValue"
17
17
  aria-describedby="helper-text-explanation"
18
- class="afcl-input inline-flex bg-lightInputBackground text-lightInputText dark:text-darkInputText border border-lightInputBorder rounded-0 shadow-customLight hover:border-lightInputBorderHover focus:outline-none focus:ring-[3px] focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:hover:border-darkInputBorderHover dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder
19
- block w-20 px-2.5 py-1.5 leading-5 dark:bg-darkInputBackground dark:border-darkInputBorder placeholder-lightInputPlaceholderText dark:placeholder-darkInputPlaceholderText dark:text-darkInputText translate-y-0 transition-colors disabled:cursor-not-allowed"
20
- :class="{'rounded-l-default': !$slots.prefix && !prefix, 'rounded-r-default': !$slots.suffix && !suffix, 'w-full': fullWidth, 'text-base': isIos, 'text-sm': !isIos, 'pr-10': $slots.rightIcon }"
18
+ :aria-invalid="invalid || undefined"
19
+ class="afcl-input inline-flex bg-lightInputBackground text-lightInputText dark:text-darkInputText border rounded-0 shadow-customLight focus:outline-none focus:ring-[3px]
20
+ block w-20 px-2.5 py-1.5 leading-5 dark:bg-darkInputBackground placeholder-lightInputPlaceholderText dark:placeholder-darkInputPlaceholderText dark:text-darkInputText translate-y-0 transition-colors disabled:cursor-not-allowed"
21
+ :class="[
22
+ invalid
23
+ ? 'border-lightDanger focus:border-lightDanger focus:ring-lightDanger/20 dark:border-darkDanger dark:focus:border-darkDanger dark:focus:ring-darkDanger/30'
24
+ : 'border-lightInputBorder hover:border-lightInputBorderHover focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:border-darkInputBorder dark:hover:border-darkInputBorderHover dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder',
25
+ {'rounded-l-default': !$slots.prefix && !prefix, 'rounded-r-default': !$slots.suffix && !suffix, 'w-full': fullWidth, 'text-base': isIos, 'text-sm': !isIos, 'pr-10': $slots.rightIcon || passwordToggle },
26
+ ]"
21
27
  :disabled="readonly"
22
28
  >
23
29
 
24
30
  <div v-if="$slots.rightIcon" class="absolute inset-y-0 right-0 pr-3 flex items-center">
25
31
  <slot name="rightIcon" />
26
32
  </div>
33
+ <div v-else-if="passwordToggle" class="absolute inset-y-0 right-0 pr-1 flex items-center">
34
+ <Button
35
+ type="button"
36
+ variant="ghost"
37
+ size="sm"
38
+ iconOnly
39
+ class="afcl-input-password-toggle"
40
+ :aria-label="revealed ? $t('Hide password') : $t('Show password')"
41
+ @click="revealed = !revealed"
42
+ >
43
+ <IconEyeOff v-if="revealed" aria-hidden="true" />
44
+ <IconEye v-else aria-hidden="true" />
45
+ </Button>
46
+ </div>
27
47
  <span
28
48
  v-if="$slots.suffix || suffix"
29
49
  class="inline-flex items-center px-2.5 text-sm text-lightInputIconColor bg-lightSecondaryDarken border border-s-0 border-lightInputBorder rounded-e-default dark:bg-darkSecondaryLighten dark:text-darkInputIconColor dark:border-darkInputBorder ">
@@ -36,8 +56,10 @@
36
56
  <script setup lang="ts">
37
57
 
38
58
  import { ref } from 'vue';
59
+ import { IconEye, IconEyeOff } from '@iconify-prerendered/vue-lucide';
39
60
  import { useCoreStore } from '@/stores/core';
40
61
  import { numberInputValue } from './inputValue';
62
+ import Button from './Button.vue';
41
63
 
42
64
  const coreStore = useCoreStore();
43
65
  const isIos = coreStore.isIos;
@@ -49,9 +71,14 @@ const props = defineProps<{
49
71
  suffix?: string,
50
72
  prefix?: string,
51
73
  readonly?: boolean,
74
+ // paints the border red, e.g. for a value which did not pass validation
75
+ invalid?: boolean,
76
+ // for type="password": a button on the right shows and hides the value
77
+ passwordToggle?: boolean,
52
78
  }>()
53
79
 
54
80
  const input = ref<HTMLInputElement | null>(null)
81
+ const revealed = ref(false)
55
82
 
56
83
  defineExpose({
57
84
  focus: () => input.value?.focus(),
@@ -19,6 +19,7 @@ export { default as ProgressBar } from './ProgressBar.vue';
19
19
  export { default as Spinner } from './Spinner.vue';
20
20
  export { default as Skeleton } from './Skeleton.vue';
21
21
  export { default as Dialog } from './Dialog.vue';
22
+ export { default as DialogPanel } from './DialogPanel.vue';
22
23
  export { default as MixedChart } from './MixedChart.vue';
23
24
  export { default as CountryFlag } from './CountryFlag.vue';
24
25
  export { default as JsonViewer } from './JsonViewer.vue';
@@ -18,6 +18,14 @@ app.use(router)
18
18
  // get access to i18n instance outside components
19
19
  initI18n(app);
20
20
 
21
+ // Chrome steps a focused number input on wheel, so scrolling the page with the cursor over the field
22
+ // silently changes a price or amount; blurring it first lets the page scroll and keeps the value
23
+ document.addEventListener('wheel', (e) => {
24
+ if (e.target instanceof HTMLInputElement && e.target.type === 'number' && e.target === document.activeElement) {
25
+ e.target.blur();
26
+ }
27
+ }, { passive: true });
28
+
21
29
 
22
30
  /* IMPORTANT:ADMINFORTH CUSTOM USES */
23
31
 
@@ -32,11 +32,15 @@
32
32
  class="af-login-modal overflow-y-auto flex flex-grow
33
33
  overflow-x-hidden z-50 min-w-[350px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
34
34
  <div class="relative p-4 w-full max-h-full max-w-[400px]">
35
- <!-- Modal content -->
36
- <div class="af-login-popup af-login-modal-content relative bg-lightLoginViewBackground rounded-default shadow dark:bg-darkLoginViewBackground dark:shadow-black" :class=" { 'rounded-b-none overflow-hidden': error } ">
37
- <!-- Modal header -->
38
- <div class="af-login-modal-header flex items-center justify-between flex-col p-4 md:p-5 border-b rounded-t dark:border-gray-600">
39
-
35
+ <DialogPanel
36
+ class="af-login-popup af-login-modal-content max-w-none bg-lightLoginViewBackground dark:bg-darkLoginViewBackground"
37
+ :error="error"
38
+ :classes="{
39
+ header: 'af-login-modal-header',
40
+ body: 'af-login-modal-body',
41
+ }"
42
+ >
43
+ <template #title>
40
44
  <template v-if="coreStore?.config?.loginPageInjections?.panelHeader.length && coreStore?.config?.loginPageInjections?.panelHeader.length > 0">
41
45
  <component
42
46
  v-for="(c, index) in coreStore?.config?.loginPageInjections?.panelHeader || []"
@@ -46,15 +50,13 @@
46
50
  @update:setLoginParam="setLoginParam"
47
51
  />
48
52
  </template>
49
- <h3 v-else class="text-xl font-semibold text-lightLoginViewText dark:text-darkLoginViewTextColor">
53
+ <p v-else class="text-lg leading-6 font-semibold text-lightLoginViewTextColor dark:text-darkLoginViewTextColor">
50
54
  {{ $t('Sign in to') }} {{ coreStore.config?.brandName }}
51
- </h3>
52
- </div>
53
- <!-- Modal body -->
54
- <div class="af-login-modal-body p-4 md:p-5">
55
- <form class="space-y-4" @submit.prevent>
55
+ </p>
56
+ </template>
57
+ <form class="space-y-4" @submit.prevent>
56
58
  <div>
57
- <label for="username" class="block mb-2 text-sm font-medium text-lightLoginViewText dark:text-darkLoginViewTextColor">{{ $t('Your') }} {{ coreStore.config?.usernameFieldName?.toLowerCase() }}</label>
59
+ <label for="username" class="block mb-2 text-sm font-medium text-lightLoginViewTextColor dark:text-darkLoginViewTextColor">{{ $t('Your') }} {{ coreStore.config?.usernameFieldName?.toLowerCase() }}</label>
58
60
  <Input
59
61
  v-model="username"
60
62
  autocomplete="username"
@@ -67,20 +69,13 @@
67
69
  placeholder="name@company.com" required />
68
70
  </div>
69
71
  <div class="">
70
- <label for="password" class="block mb-2 text-sm font-medium text-lightLoginViewText dark:text-darkLoginViewTextColor">{{ $t('Your password') }}</label>
72
+ <label for="password" class="block mb-2 text-sm font-medium text-lightLoginViewTextColor dark:text-darkLoginViewTextColor">{{ $t('Your password') }}</label>
71
73
  <Input
72
74
  v-model="password"
73
75
  ref="passwordInput"
74
76
  autocomplete="current-password"
75
77
  @keydown.enter="login"
76
- :type="!showPw ? 'password': 'text'" name="password" id="password" placeholder="••••••••" class="w-full" required>
77
- <template #rightIcon>
78
- <button type="button" @click="showPw = !showPw" class="text-lightLoginViewSubTextColor dark:text-darkLoginViewSubTextColor">
79
- <IconEye class="w-4 h-4" v-if="!showPw" />
80
- <IconEyeOff class="w-4 h-4" v-else />
81
- </button>
82
- </template>
83
- </Input>
78
+ type="password" passwordToggle name="password" id="password" placeholder="••••••••" class="w-full" required />
84
79
  </div>
85
80
 
86
81
  <div v-if="coreStore?.config?.rememberMeDuration"
@@ -123,10 +118,8 @@
123
118
  @update:setLoginParam="setLoginParam"
124
119
  @update:oauthRedirecting="oauthRedirecting = $event"
125
120
  />
126
- </form>
127
- </div>
128
- </div>
129
- <ErrorMessage v-if="error" :error="error" class="absolute left-4 right-4 rounded-t-none mb-0 shadow px-9" />
121
+ </form>
122
+ </DialogPanel>
130
123
  </div>
131
124
  </div>
132
125
 
@@ -140,11 +133,10 @@ import { getCustomComponent, formatComponent } from '@/utils';
140
133
  import { onMounted, ref, computed } from 'vue';
141
134
  import { useCoreStore } from '@/stores/core';
142
135
  import { useUserStore } from '@/stores/user';
143
- import { IconEye, IconEyeOff, IconInfo } from '@iconify-prerendered/vue-lucide';
136
+ import { IconInfo } from '@iconify-prerendered/vue-lucide';
144
137
  import { callAdminForthApi, loadFile } from '@/utils';
145
138
  import { useRouter, useRoute } from 'vue-router';
146
- import { Button, Checkbox, Input, Spinner } from '@/afcl';
147
- import ErrorMessage from '@/components/ErrorMessage.vue';
139
+ import { Button, Checkbox, DialogPanel, Input, Spinner } from '@/afcl';
148
140
 
149
141
  const passwordInput = ref<InstanceType<typeof Input> | null>(null);
150
142
  const usernameInput = ref<InstanceType<typeof Input> | null>(null);
@@ -161,7 +153,6 @@ const isSuccess = ref<boolean>(false);
161
153
  const coreStore = useCoreStore();
162
154
  const user = useUserStore();
163
155
 
164
- const showPw = ref(false);
165
156
 
166
157
  const error = ref(null);
167
158
  const disableLoginButton = ref(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "4.5.1",
3
+ "version": "4.6.1",
4
4
  "description": "OpenSource Agent-Native forth-generation admin panel",
5
5
  "keywords": [
6
6
  "adminforth",