@citruslime/ui 1.2.1-beta.0 → 2.0.0-beta.3
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/.eslintrc.js +8 -2
- package/dist/@types/appUser.d.ts +1 -0
- package/dist/@types/components/grid/column.d.ts +2 -1
- package/dist/@types/components/header/index.d.ts +0 -1
- package/dist/@types/components/{header/navigation.d.ts → navigation/index.d.ts} +7 -4
- package/dist/@types/index.d.ts +1 -0
- package/dist/components/index.d.ts +17 -14
- package/dist/main.d.ts +1 -1
- package/dist/style.css +1 -1
- package/dist/theme.js +2 -4
- package/dist/ui.es.js +1 -1
- package/dist/ui.umd.js +1 -1
- package/package.json +7 -4
- package/src/components/accordion/cl-ui-accordion.vue +89 -0
- package/src/components/app/cl-ui-app.vue +35 -0
- package/src/components/button/{button.vue → cl-ui-button.vue} +26 -6
- package/src/components/calendar/cl-ui-calendar.vue +277 -0
- package/src/components/card/{card.vue → cl-ui-card.vue} +17 -1
- package/src/components/combo-box/cl-ui-combo-box.vue +357 -0
- package/src/components/combo-box/search-container/cl-ui-combo-box-search.vue +279 -0
- package/src/components/combo-box/search-container/{header-option/header-option.vue → header/cl-ui-combo-box-header.vue} +17 -2
- package/src/components/combo-box/search-container/selectable/cl-ui-combo-box-selectable.vue +99 -0
- package/src/components/footer/{footer.vue → cl-ui-footer.vue} +10 -2
- package/src/components/grid/cell/{cell.vue → cl-ui-grid-cell.vue} +90 -1
- package/src/components/grid/cl-ui-grid.vue +477 -0
- package/src/components/grid/filter/cl-ui-grid-filter.vue +270 -0
- package/src/components/grid/footer/{footer.vue → cl-ui-grid-footer.vue} +100 -5
- package/src/components/grid/header/cl-ui-grid-header.vue +76 -0
- package/src/components/grid/view-manager/cl-ui-grid-view-manager.vue +145 -0
- package/src/components/header/cl-ui-header.vue +11 -0
- package/src/components/header-helper/cl-ui-header-helper.vue +50 -0
- package/src/components/language-switcher/{language-switcher.vue → cl-ui-language-switcher.vue} +49 -3
- package/src/components/loading-spinner/cl-ui-loading-spinner.vue +16 -0
- package/src/components/login/{login.vue → cl-ui-login.vue} +101 -19
- package/src/components/modal/{modal.vue → cl-ui-modal.vue} +74 -2
- package/src/components/navigation/cl-ui-navigation.vue +124 -0
- package/src/components/notification/{notification.vue → cl-ui-notification.vue} +21 -2
- package/src/components/slider/cl-ui-slider.vue +145 -0
- package/src/components/accordion/accordion.vue +0 -30
- package/src/components/calendar/calendar.vue +0 -35
- package/src/components/combo-box/combo-box.vue +0 -79
- package/src/components/combo-box/search-container/search-container.vue +0 -57
- package/src/components/combo-box/search-container/selectable-option/selectable-option.vue +0 -27
- package/src/components/grid/filter/filter.vue +0 -93
- package/src/components/grid/grid.vue +0 -194
- package/src/components/grid/header/header.vue +0 -39
- package/src/components/grid/view-manager/view-manager.vue +0 -73
- package/src/components/header/header-helper/header-helper.vue +0 -95
- package/src/components/header/header.vue +0 -33
- package/src/components/header/navigation/navigation.vue +0 -84
- package/src/components/loading-spinner/loading-spinner.vue +0 -8
- package/src/components/slider/slider.vue +0 -41
package/src/components/language-switcher/{language-switcher.vue → cl-ui-language-switcher.vue}
RENAMED
|
@@ -1,6 +1,52 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import { isLanguageLocaleFormat, Language } from '../../@types';
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
currentLocale: string;
|
|
8
|
+
supportedLocales: Language[];
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}>(), {
|
|
11
|
+
disabled: false
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
defineEmits({
|
|
15
|
+
'update:current-locale': null
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const localeToggleOpen = ref<boolean>(false);
|
|
19
|
+
|
|
20
|
+
const selectedLanguage = computed<Language | null>(() => props.supportedLocales.find(loc => loc.localeCode === props.currentLocale) ?? null);
|
|
21
|
+
|
|
22
|
+
const validLanguages = computed<Language[]>(() => props.supportedLocales.filter(e => isLanguageLocaleFormat(e)).sort((a,b) => (a.nativeName > b.nativeName) ? 1 : ((b.nativeName > a.nativeName) ? -1 : 0)));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Toggles whether the locale dropdown is shown or hidden.
|
|
26
|
+
*/
|
|
27
|
+
function toggleLocaleSwitcher (): void {
|
|
28
|
+
if (!props.disabled) {
|
|
29
|
+
localeToggleOpen.value = !localeToggleOpen.value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
watch(() => props.disabled, (newValue) => {
|
|
34
|
+
if (newValue) {
|
|
35
|
+
localeToggleOpen.value = false;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<style scoped lang="postcss">
|
|
41
|
+
.flag-svg,
|
|
42
|
+
.flag-svg[data-size='small'] {
|
|
43
|
+
background-position: center center !important;
|
|
44
|
+
background-repeat: no-repeat !important;
|
|
45
|
+
background-size: cover !important;
|
|
46
|
+
height: 12px;
|
|
47
|
+
width: 16px;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
4
50
|
|
|
5
51
|
<template>
|
|
6
52
|
<div v-if="selectedLanguage !== null && isLanguageLocaleFormat(selectedLanguage)"
|
|
@@ -23,7 +69,7 @@
|
|
|
23
69
|
class="border-b border-grey-0 content-center cursor-pointer flex hover:bg-grey-0 items-center last-child:border-b-0 last:border-b-0 p-2 text-center transition-colors"
|
|
24
70
|
:data-localename="location.name"
|
|
25
71
|
:data-localecode="location.localeCode"
|
|
26
|
-
@click="
|
|
72
|
+
@click="$emit('update:current-locale', location.localeCode)">
|
|
27
73
|
<span class="flag-svg inline-block mr-3"
|
|
28
74
|
:style="{ 'background': `url('${location.svgCode}')` }"></span>
|
|
29
75
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<style scoped lang="postcss">
|
|
2
|
+
.loading-spinner {
|
|
3
|
+
@apply h-8 w-8;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.loading-spinner:after {
|
|
7
|
+
@apply absolute animate-spin border-2 border-primary-default rounded-full h-8 w-8;
|
|
8
|
+
|
|
9
|
+
border-bottom: 2px solid transparent;
|
|
10
|
+
content: '';
|
|
11
|
+
}
|
|
12
|
+
</style>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="loading-spinner"></div>
|
|
16
|
+
</template>
|
|
@@ -1,6 +1,82 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import { Authentication, Language, LoginLocalisations } from '../../@types';
|
|
5
|
+
import ClUiLanguageSwitcher from '../language-switcher/cl-ui-language-switcher.vue';
|
|
6
|
+
|
|
7
|
+
type PasswordFieldType = 'text' | 'password';
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<{
|
|
10
|
+
errors?: string[];
|
|
11
|
+
currentLocale?: string;
|
|
12
|
+
supportedLocales: Language[];
|
|
13
|
+
localisations?: LoginLocalisations;
|
|
14
|
+
logo: string;
|
|
15
|
+
backgroundImage: string;
|
|
16
|
+
loading?: false;
|
|
17
|
+
}>(), {
|
|
18
|
+
errors: () => [],
|
|
19
|
+
currentLocale: 'en-GB',
|
|
20
|
+
localisations: () => ({
|
|
21
|
+
login: 'Login',
|
|
22
|
+
email: 'Email Address',
|
|
23
|
+
password: 'Password',
|
|
24
|
+
validEmail: 'Username is a valid email address.',
|
|
25
|
+
invalidEmail: 'Username must be a valid email address.'
|
|
26
|
+
}),
|
|
27
|
+
loading: false
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits({
|
|
31
|
+
login: null,
|
|
32
|
+
'update:errors': null,
|
|
33
|
+
'update:current-locale': null
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const username = ref('');
|
|
37
|
+
const password = ref('');
|
|
38
|
+
|
|
39
|
+
const passwordFieldType = ref<PasswordFieldType>('password');
|
|
40
|
+
const usernameValid = ref<boolean>(false);
|
|
41
|
+
|
|
42
|
+
const selectedLocale = computed<string>({
|
|
43
|
+
get: () => props.currentLocale,
|
|
44
|
+
set: (value) => emit('update:current-locale', value)
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Emits the current username and password values.
|
|
49
|
+
*/
|
|
50
|
+
function login (): void {
|
|
51
|
+
emit('login', {
|
|
52
|
+
username: username.value,
|
|
53
|
+
password: password.value
|
|
54
|
+
} as Authentication);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Emits an event to clear the currently displayed error.
|
|
59
|
+
*/
|
|
60
|
+
function clearErrors (): void {
|
|
61
|
+
emit('update:errors', []);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
watch(username, () => {
|
|
65
|
+
const emailRegex = /[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)/gi;
|
|
66
|
+
|
|
67
|
+
usernameValid.value = emailRegex.test(username.value);
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<style scoped lang="postcss">
|
|
72
|
+
*[data-valid='true'] {
|
|
73
|
+
@apply border-2 border-primary-default;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
*[data-valid='false'] {
|
|
77
|
+
@apply border-2 border-danger-default;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
4
80
|
|
|
5
81
|
<template>
|
|
6
82
|
<div class="bg-cover bg-left-bottom h-screen"
|
|
@@ -17,8 +93,9 @@
|
|
|
17
93
|
|
|
18
94
|
<div class="md:pb-0 md:pr-2 md:w-1/4 pb-2 w-full">
|
|
19
95
|
<div class="inline relative text-grey-4 w-full">
|
|
20
|
-
<
|
|
21
|
-
|
|
96
|
+
<icon class="absolute left-3 top-0.5"
|
|
97
|
+
icon="ph:user"
|
|
98
|
+
:size="18" />
|
|
22
99
|
|
|
23
100
|
<input v-model.trim="username"
|
|
24
101
|
class="!pb-1 !pl-9 w-full"
|
|
@@ -33,8 +110,9 @@
|
|
|
33
110
|
class="text-grey-3 text-sm">
|
|
34
111
|
<div v-show="usernameValid"
|
|
35
112
|
class="flex mt-2">
|
|
36
|
-
<
|
|
37
|
-
|
|
113
|
+
<icon class="text-primary-default"
|
|
114
|
+
icon="ph:check"
|
|
115
|
+
:size="16" />
|
|
38
116
|
|
|
39
117
|
<em class="align-middle inline-block ml-4">
|
|
40
118
|
{{ localisations.validEmail }}
|
|
@@ -43,7 +121,8 @@
|
|
|
43
121
|
|
|
44
122
|
<div v-show="!usernameValid"
|
|
45
123
|
class="flex mt-2">
|
|
46
|
-
<
|
|
124
|
+
<icon class="text-danger-default"
|
|
125
|
+
icon="ph:x"
|
|
47
126
|
:size="16" />
|
|
48
127
|
|
|
49
128
|
<em class="align-middle inline-block ml-4">
|
|
@@ -55,18 +134,21 @@
|
|
|
55
134
|
|
|
56
135
|
<div class="md:pb-0 md:pr-2 md:w-1/4 pb-2 w-full">
|
|
57
136
|
<div class="inline relative text-grey-4 w-full">
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
137
|
+
<icon class="absolute left-3 top-0.5"
|
|
138
|
+
icon="ph:lock"
|
|
139
|
+
:size="18" />
|
|
140
|
+
|
|
141
|
+
<icon v-show="passwordFieldType === 'password'"
|
|
142
|
+
class="absolute cursor-pointer right-3 top-0.5"
|
|
143
|
+
icon="ph:eye"
|
|
144
|
+
:size="18"
|
|
145
|
+
@click="passwordFieldType = 'text'" />
|
|
146
|
+
|
|
147
|
+
<icon v-show="passwordFieldType === 'text'"
|
|
148
|
+
class="absolute cursor-pointer right-3 top-0.5"
|
|
149
|
+
icon="ph:eye-slash"
|
|
150
|
+
:size="18"
|
|
151
|
+
@click="passwordFieldType = 'password'" />
|
|
70
152
|
|
|
71
153
|
<input v-model="password"
|
|
72
154
|
class="!pb-1 !pl-9 !pr-9 w-full"
|
|
@@ -1,4 +1,75 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export default {
|
|
3
|
+
inheritAttrs: false
|
|
4
|
+
};
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { computed, ref } from 'vue';
|
|
9
|
+
|
|
10
|
+
type ModalSize = 'x-small' | 'small' | 'medium' | 'large';
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
|
+
title: string;
|
|
14
|
+
size?: ModalSize;
|
|
15
|
+
confirmButton?: string;
|
|
16
|
+
cancelButton?: string;
|
|
17
|
+
confirmEnabled?: boolean;
|
|
18
|
+
}>(), {
|
|
19
|
+
size: 'medium',
|
|
20
|
+
confirmButton: '',
|
|
21
|
+
cancelButton: '',
|
|
22
|
+
confirmEnabled: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits({
|
|
26
|
+
confirm: null,
|
|
27
|
+
cancel: null
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const visible = ref<boolean>(false);
|
|
31
|
+
|
|
32
|
+
const showConfirmButton = computed(() => props.confirmButton !== '');
|
|
33
|
+
const showCancelButton = computed(() => props.cancelButton !== '');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gets the text for a button, either the default or overridden text.
|
|
37
|
+
*
|
|
38
|
+
* @param buttonValue Enable/disable flag or button override text.
|
|
39
|
+
* @returns The label value to use.
|
|
40
|
+
*/
|
|
41
|
+
function getButtonText (buttonValue: string | boolean): string {
|
|
42
|
+
return typeof buttonValue === 'string' ? buttonValue : '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const confirmButtonText = computed(() => getButtonText(props.confirmButton));
|
|
46
|
+
const cancelButtonText = computed(() => getButtonText(props.cancelButton));
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Toggles the visibility of the model, either to the opposite of its current value or to the provided value.
|
|
50
|
+
*
|
|
51
|
+
* @param show Whether to show or hide the modal.
|
|
52
|
+
*/
|
|
53
|
+
function toggleModal (show?: boolean): void {
|
|
54
|
+
visible.value = typeof show !== 'undefined' ? show : !visible.value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Emits an event depending on if the modal has been confirmed or cancelled.
|
|
59
|
+
*
|
|
60
|
+
* @param action Whether the confirm or cancel/close button has been selected.
|
|
61
|
+
*/
|
|
62
|
+
function modalAction (action: boolean): void {
|
|
63
|
+
toggleModal(false);
|
|
64
|
+
|
|
65
|
+
if (action) {
|
|
66
|
+
emit('confirm', action);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
emit('cancel', action);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
2
73
|
|
|
3
74
|
<template>
|
|
4
75
|
<slot name="trigger"
|
|
@@ -12,7 +83,8 @@
|
|
|
12
83
|
:class="{ 'lg:w-2/12 w-10/12': size === 'x-small', 'w-6/12': size === 'small', 'w-8/12': size === 'medium', 'w-10/12': size === 'large' }">
|
|
13
84
|
<div class="absolute cursor-pointer h-2 leading-5 right-3 text-black text-center top-3 w-5"
|
|
14
85
|
@click="modalAction(false)">
|
|
15
|
-
<
|
|
86
|
+
<icon icon="ph:x"
|
|
87
|
+
:size="20" />
|
|
16
88
|
</div>
|
|
17
89
|
|
|
18
90
|
<div class="border-b border-grey-2 font-semibold h-11 leading-10 overflow-ellipsis overflow-hidden pl-2 pr-8 text-2xl w-full whitespace-nowrap">
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
import { NavigationGroup } from '../../@types';
|
|
5
|
+
|
|
6
|
+
withDefaults(defineProps<{
|
|
7
|
+
groups?: NavigationGroup[];
|
|
8
|
+
}>(), {
|
|
9
|
+
groups: () => []
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
defineEmits({
|
|
13
|
+
'item-select': null
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const activeGroup = ref<NavigationGroup | null>(null);
|
|
17
|
+
const menuPinned = ref(false);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Selects a given navigation group.
|
|
21
|
+
*
|
|
22
|
+
* @param group The group which has been selected.
|
|
23
|
+
*/
|
|
24
|
+
function selectNavigationGroup (group: NavigationGroup): void {
|
|
25
|
+
activeGroup.value = (activeGroup.value?.name === group.name ? null : group);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Toggles the pinned state of the navigation.
|
|
30
|
+
*/
|
|
31
|
+
function toggleMenuPinned (): void {
|
|
32
|
+
menuPinned.value = !menuPinned.value;
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<style scoped lang="postcss">
|
|
37
|
+
.navslide-enter-from,
|
|
38
|
+
.navslide-leave-to {
|
|
39
|
+
@apply -left-full;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#group-container {
|
|
43
|
+
scrollbar-width: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#group-container::-webkit-scrollbar {
|
|
47
|
+
@apply hidden;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div class="flex flex-nowrap h-full max-h-full min-h-full min-w-20 relative self-start w-auto">
|
|
53
|
+
<div v-if="groups"
|
|
54
|
+
id="group-container"
|
|
55
|
+
class="bg-blue-default h-full left-0 max-h-full min-h-full min-w-20 overflow-y-auto self-start sticky w-20 z-30">
|
|
56
|
+
<div v-for="(group, index) in groups"
|
|
57
|
+
:key="index"
|
|
58
|
+
class="flex-grow w-full">
|
|
59
|
+
<div class="cursor-pointer h-20 hover:bg-blue-light transition-colors w-20"
|
|
60
|
+
:class="{ 'bg-blue-light' : group.name === activeGroup?.name }"
|
|
61
|
+
@click="selectNavigationGroup(group);">
|
|
62
|
+
<icon v-if="group.icon !== null"
|
|
63
|
+
class="block h-20 p-7 w-20"
|
|
64
|
+
:icon="group.icon"
|
|
65
|
+
color="#FFFFFF" />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<transition name="navslide">
|
|
71
|
+
<div v-if="activeGroup"
|
|
72
|
+
class="bg-grey-0 border-grey-2 border-r max-h-full min-h-full min-w-[260px] p-8 self-start top-0 transition-all z-20"
|
|
73
|
+
:class="{ 'relative' : menuPinned, 'absolute left-20': !menuPinned }">
|
|
74
|
+
<div class="font-bold leading-8 mb-8 pr-8 relative w-full">
|
|
75
|
+
<span class="select-none text-blue-light text-lg">
|
|
76
|
+
{{ activeGroup.caption }}
|
|
77
|
+
</span>
|
|
78
|
+
|
|
79
|
+
<span class="absolute cursor-pointer h-8 hidden leading-8 md:inline-block right-0 text-center top-0 w-8"
|
|
80
|
+
@click="toggleMenuPinned">
|
|
81
|
+
<icon v-if="!menuPinned"
|
|
82
|
+
class="inline-block"
|
|
83
|
+
icon="ph:push-pin" />
|
|
84
|
+
<icon v-else
|
|
85
|
+
class="inline-block"
|
|
86
|
+
icon="ph:push-pin-slash" />
|
|
87
|
+
</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div v-for="(section, sectionIndex) in activeGroup.sections"
|
|
90
|
+
:key="sectionIndex"
|
|
91
|
+
class="mb-8 text-sm w-full">
|
|
92
|
+
<div class="font-bold mb-4 select-none w-full">
|
|
93
|
+
{{ section.caption }}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<span v-for="(record, recordIndex) in section.records"
|
|
97
|
+
:key="recordIndex"
|
|
98
|
+
class="block cursor-pointer hover:text-blue-light mb-2 transition-colors">
|
|
99
|
+
<template v-if="record.component">
|
|
100
|
+
<component :is="record.component"
|
|
101
|
+
:to="record.link"
|
|
102
|
+
@click="$emit('item-select', record)">
|
|
103
|
+
{{ record.caption }}
|
|
104
|
+
</component>
|
|
105
|
+
</template>
|
|
106
|
+
<a v-else
|
|
107
|
+
:href="record.link"
|
|
108
|
+
:alt="`External link to: ${record.link}`"
|
|
109
|
+
:title="`External link to: ${record.link}`"
|
|
110
|
+
target="new"
|
|
111
|
+
@click="$emit('item-select', record)">
|
|
112
|
+
<icon
|
|
113
|
+
class="inline-block"
|
|
114
|
+
icon="ph:arrow-square-out"
|
|
115
|
+
alt="External Link" />
|
|
116
|
+
|
|
117
|
+
{{ record.caption }}
|
|
118
|
+
</a>
|
|
119
|
+
</span>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</transition>
|
|
123
|
+
</div>
|
|
124
|
+
</template>
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export default {
|
|
3
|
+
inheritAttrs: false
|
|
4
|
+
};
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { notifications, removeNotification } from '../../composables/notification';
|
|
9
|
+
|
|
10
|
+
type NotificationPosition = 'top-left' | 'top-centre' | 'top-right' | 'bottom-left' | 'bottom-centre' | 'bottom-right';
|
|
11
|
+
|
|
12
|
+
withDefaults(defineProps<{
|
|
13
|
+
container?: string;
|
|
14
|
+
position?: NotificationPosition;
|
|
15
|
+
}>(), {
|
|
16
|
+
container: 'body',
|
|
17
|
+
position: 'top-right'
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
2
20
|
|
|
3
21
|
<template>
|
|
4
22
|
<teleport :to="container">
|
|
@@ -27,7 +45,8 @@
|
|
|
27
45
|
{{ notification.message }}
|
|
28
46
|
</span>
|
|
29
47
|
|
|
30
|
-
<
|
|
48
|
+
<icon class="group-hover:opacity-100 inline-block opacity-0"
|
|
49
|
+
icon="ph:x"
|
|
31
50
|
:size="16"
|
|
32
51
|
weight="bold" />
|
|
33
52
|
</div>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick } from 'vue';
|
|
3
|
+
import { useI18n } from 'vue-i18n';
|
|
4
|
+
|
|
5
|
+
import { NumberFormat, SliderLocalisations } from '../../@types';
|
|
6
|
+
import { useDebouncer } from '../../utils';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<{
|
|
9
|
+
value: number;
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
currentLocale?: string;
|
|
13
|
+
localisations?: SliderLocalisations;
|
|
14
|
+
step?: number;
|
|
15
|
+
enforceStep?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}>(), {
|
|
18
|
+
currentLocale: 'en-GB',
|
|
19
|
+
localisations: () => ({
|
|
20
|
+
invalidProps: 'The current combination of props is invalid. Please confirm the values provided are correct.'
|
|
21
|
+
}),
|
|
22
|
+
step: 1,
|
|
23
|
+
enforceStep: false,
|
|
24
|
+
disabled: false
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits({
|
|
28
|
+
'update:value': null
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { n } = useI18n();
|
|
32
|
+
const { debounce } = useDebouncer();
|
|
33
|
+
|
|
34
|
+
const currentValue = computed<number>({
|
|
35
|
+
get: () => props.value,
|
|
36
|
+
set: (value) => emit('update:value', value)
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const colour = computed<string>(() => props.disabled ? 'rgba(153, 153, 153, 0.8)' : '#9acd32');
|
|
40
|
+
const percentage = computed<number>(() => {
|
|
41
|
+
let value = ((currentValue.value - props.min) / (props.max - props.min)) * 100;
|
|
42
|
+
|
|
43
|
+
if (value < 35 && value > 0) {
|
|
44
|
+
if (value < 20) {
|
|
45
|
+
value += 0.5;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
value += 0.25;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (value > 65 && value < 100) {
|
|
52
|
+
if (value > 80) {
|
|
53
|
+
value -= 0.5;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
value -= 0.25;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return value;
|
|
61
|
+
});
|
|
62
|
+
const validProps = computed<boolean>(() => props.min <= props.max && props.step > 0);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Updates the current value and forces a re-render of the input.
|
|
66
|
+
*
|
|
67
|
+
* @param target The input event target.
|
|
68
|
+
* @param forceUpdate $forceUpdate isn't available in the composition API but can be passed in as a parameter.
|
|
69
|
+
*/
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
71
|
+
function updateCurrentValue (target: EventTarget | null, forceUpdate: Function): void {
|
|
72
|
+
const inputValue: string = (target as HTMLInputElement | null)?.value ?? '';
|
|
73
|
+
const value: number = Math.max(Math.min(parseFloat(inputValue) || props.min, props.max), props.min);
|
|
74
|
+
|
|
75
|
+
currentValue.value = props.enforceStep ? Math.ceil(value / props.step) * props.step : value;
|
|
76
|
+
|
|
77
|
+
nextTick(() => forceUpdate());
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<style scoped lang="postcss">
|
|
82
|
+
input[type='range']::-webkit-slider-runnable-track {
|
|
83
|
+
@apply bg-none h-1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
input[type='range']::-moz-range-track {
|
|
87
|
+
@apply bg-none rounded-full h-1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
input[type='range']::-webkit-slider-thumb {
|
|
91
|
+
@apply appearance-none bg-white border border-solid border-grey-2 rounded-full cursor-pointer h-6 w-6;
|
|
92
|
+
|
|
93
|
+
margin-top: -0.6rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
input[type='range']:disabled::-webkit-slider-thumb {
|
|
97
|
+
@apply hidden;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
input[type='range']::-moz-range-thumb {
|
|
101
|
+
@apply bg-white border border-grey-2 rounded-full cursor-pointer h-6 w-6;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
input[type='range']:disabled::-moz-range-thumb {
|
|
105
|
+
@apply h-0 w-0;
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<div v-if="validProps"
|
|
111
|
+
v-bind="$attrs"
|
|
112
|
+
class="flex flex-wrap items-center">
|
|
113
|
+
<input class="!text-sm md:!h-8 md:!w-auto"
|
|
114
|
+
type="number"
|
|
115
|
+
:value="currentValue"
|
|
116
|
+
:min="min"
|
|
117
|
+
:max="max"
|
|
118
|
+
:step="step"
|
|
119
|
+
:disabled="disabled"
|
|
120
|
+
@input="debounce(updateCurrentValue, [ $event.target, $forceUpdate ])">
|
|
121
|
+
|
|
122
|
+
<div class="flex flex-1 flex-wrap items-center md:mt-0 mt-3">
|
|
123
|
+
<span class="bg-grey-0 leading-6 md:ml-2 ml-0 px-3 rounded-full text-center text-xs">
|
|
124
|
+
{{ n(min, Number.isInteger(min) ? NumberFormat.INTEGER : NumberFormat.DECIMAL, currentLocale ) }}
|
|
125
|
+
</span>
|
|
126
|
+
|
|
127
|
+
<input v-model.number="currentValue"
|
|
128
|
+
class="align-middle appearance-none bg-gradient-to-r border border-grey-2 delay-500 ease-in flex-1 h-5 ml-2 outline-none rounded-full transition-colors"
|
|
129
|
+
:style="{ background: `linear-gradient(to right, ${colour} 0%, ${colour} ${percentage}%, white ${percentage}%, white 100%)` }"
|
|
130
|
+
type="range"
|
|
131
|
+
:min="min"
|
|
132
|
+
:max="max"
|
|
133
|
+
:step="step"
|
|
134
|
+
:disabled="disabled">
|
|
135
|
+
|
|
136
|
+
<span class="bg-grey-0 leading-6 ml-2 px-3 rounded-full text-center text-xs">
|
|
137
|
+
{{ n(max, Number.isInteger(max) ? NumberFormat.INTEGER : NumberFormat.DECIMAL, currentLocale ) }}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div v-else
|
|
142
|
+
class="emphasis-danger text-sm w-full">
|
|
143
|
+
{{ localisations.invalidProps }}
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
<script lang="ts" src="./accordion"></script>
|
|
2
|
-
|
|
3
|
-
<template>
|
|
4
|
-
<ul class="border border-grey-2">
|
|
5
|
-
<li v-for="(item, index) in itemList"
|
|
6
|
-
:key="index"
|
|
7
|
-
class="relative">
|
|
8
|
-
<div class="border border-grey-2 cursor-pointer ease-in-out focus:border-blue-light focus:outline-none font-semibold hover:scale-101 p-4 scale-100 transform-gpu transition-transform"
|
|
9
|
-
:class="{
|
|
10
|
-
'border-b-2': item.open,
|
|
11
|
-
[`bg-${titleBackground}`]: true
|
|
12
|
-
}"
|
|
13
|
-
:tabindex="index + 1"
|
|
14
|
-
@click="toggleItem(item)"
|
|
15
|
-
@keyup.enter="toggleItem(item)">
|
|
16
|
-
{{ item.title }}
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<div v-show="item.open"
|
|
20
|
-
class="border-b border-grey-2 p-6 text-sm"
|
|
21
|
-
:class="[ `bg-${contentBackground}` ]">
|
|
22
|
-
<slot :name="item.id"></slot>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<ph-caret-down class="absolute cursor-pointer right-4 top-4 transform transition-transform"
|
|
26
|
-
:class="{ 'rotate-180': item.open, 'rotate-0': !item.open }"
|
|
27
|
-
@click="toggleItem(item)" />
|
|
28
|
-
</li>
|
|
29
|
-
</ul>
|
|
30
|
-
</template>
|