design-system-next 1.8.2 → 1.9.2

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,64 @@
1
+ <template>
2
+ <div class="spr-font-main">
3
+ <template v-if="props.groupItemsBy">
4
+ <div class="spr-grid spr-gap-2">
5
+ <div v-for="(items, labelIndex) in groupedMenuList" :key="labelIndex" class="spr-grid spr-gap-0.5">
6
+ <label v-if="labelIndex" class="spr-label-sm-regular spr-text-color-base spr-p-2">
7
+ {{ labelIndex }}
8
+ </label>
9
+ <div
10
+ v-for="(item, itemIndex) in items"
11
+ :key="itemIndex"
12
+ :class="getListItemClasses(item)"
13
+ @click="handleSelectedItem(item)"
14
+ >
15
+ <spr-checkbox v-if="props.multiSelect" :checked="isItemSelected(item)" />
16
+ <div class="spr-flex spr-w-full spr-items-center">
17
+ <span class="spr-text-xs">{{ item.text }}</span>
18
+ </div>
19
+ <Icon
20
+ v-if="isItemSelected(item) && !props.multiSelect"
21
+ class="spr-text-color-brand-base spr-w-[1.39em]"
22
+ icon="ph:check"
23
+ />
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </template>
28
+ <template v-else>
29
+ <div
30
+ v-for="(item, index) in initialMenuList"
31
+ :key="index"
32
+ :class="getListItemClasses(item)"
33
+ @click="handleSelectedItem(item)"
34
+ >
35
+ <spr-checkbox v-if="props.multiSelect" :checked="isItemSelected(item)" />
36
+ <div class="spr-flex spr-w-full spr-items-center">
37
+ <span class="spr-text-xs">{{ item.text }}</span>
38
+ </div>
39
+ <Icon
40
+ v-if="isItemSelected(item) && !props.multiSelect"
41
+ class="spr-text-color-brand-base spr-w-[1.39em]"
42
+ icon="ph:check"
43
+ />
44
+ </div>
45
+ </template>
46
+ </div>
47
+ </template>
48
+
49
+ <script lang="ts" setup>
50
+ import { Icon } from '@iconify/vue';
51
+
52
+ import { listPropTypes, listEmitTypes } from './list';
53
+ import { useList } from './use-list';
54
+
55
+ import SprCheckbox from '../checkbox/checkbox.vue';
56
+
57
+ const props = defineProps(listPropTypes);
58
+ const emit = defineEmits(listEmitTypes);
59
+
60
+ const { initialMenuList, groupedMenuList, isItemSelected, getListItemClasses, handleSelectedItem } = useList(
61
+ props,
62
+ emit,
63
+ );
64
+ </script>
@@ -0,0 +1,149 @@
1
+ import { ref, toRefs, computed, ComputedRef, onMounted, watch } from 'vue';
2
+
3
+ import classNames from 'classnames';
4
+
5
+ import type { SetupContext } from 'vue';
6
+ import type { ListPropTypes, ListEmitTypes } from './list';
7
+
8
+ interface ListClasses {
9
+ listItemClasses: string;
10
+ }
11
+
12
+ interface SelectedItem {
13
+ text: string;
14
+ value: string | number;
15
+ }
16
+
17
+ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>['emit']) => {
18
+ const { modelValue, menuList, groupItemsBy, multiSelect } = toRefs(props);
19
+
20
+ const listClasses: ComputedRef<ListClasses> = computed(() => {
21
+ const listItemClasses = classNames(
22
+ 'spr-flex spr-cursor-pointer spr-items-center spr-justify-between spr-gap-1.5 spr-rounded-lg spr-p-2',
23
+ 'spr-transition spr-duration-150 spr-ease-in-out',
24
+ 'hover:spr-background-color-hover',
25
+ 'active:spr-background-color-single-active active:spr-scale-[.98]',
26
+ );
27
+
28
+ return { listItemClasses };
29
+ });
30
+
31
+ const initialMenuList = ref<{ text: string; value: string }[]>([]);
32
+
33
+ const groupedMenuList = ref<{ [key: string]: { text: string; value: string }[] }>({});
34
+
35
+ const selectedItems = ref<SelectedItem[]>([]);
36
+
37
+ // #region - Helper Methods
38
+ const isItemSelected = (item: SelectedItem) => {
39
+ return selectedItems.value.some((selectedItem) => selectedItem.text === item.text);
40
+ };
41
+
42
+ const setMenuList = () => {
43
+ if (menuList.value && menuList.value.length > 0) {
44
+ initialMenuList.value = menuList.value;
45
+
46
+ if (groupItemsBy?.value) {
47
+ setGroupedMenuList();
48
+ }
49
+ }
50
+ };
51
+
52
+ const setGroupedMenuList = () => {
53
+ if (!groupItemsBy?.value) return;
54
+
55
+ const groupedItems: { [key: string]: { text: string; value: string }[] } = {};
56
+
57
+ initialMenuList.value.forEach((item) => {
58
+ const firstCharacter = item.text.charAt(0);
59
+ const groupKey = /^\d/.test(firstCharacter) ? '' : firstCharacter.toUpperCase();
60
+
61
+ if (!groupedItems[groupKey]) {
62
+ groupedItems[groupKey] = [];
63
+ }
64
+
65
+ groupedItems[groupKey].push(item);
66
+ });
67
+
68
+ const sortedGroupedItems = Object.keys(groupedItems)
69
+ .sort((a, b) => {
70
+ if (a === '') return -1;
71
+ if (b === '') return 1;
72
+ return groupItemsBy.value === 'Z-A' ? b.localeCompare(a) : a.localeCompare(b);
73
+ })
74
+ .reduce((result: { [key: string]: { text: string; value: string }[] }, key) => {
75
+ const sortedItems = groupedItems[key].sort((a, b) => {
76
+ const comparison = a.text.localeCompare(b.text);
77
+ return groupItemsBy.value === 'Z-A' ? -comparison : comparison;
78
+ });
79
+
80
+ result[key] = sortedItems;
81
+
82
+ return result;
83
+ }, {});
84
+
85
+ groupedMenuList.value = sortedGroupedItems;
86
+ };
87
+
88
+ const setPreSelectedItems = () => {
89
+ if (modelValue.value && modelValue.value.length > 0) {
90
+ modelValue.value.forEach((preSelectedItem: string) => {
91
+ const item = initialMenuList.value.find(
92
+ (menuItem) =>
93
+ String(menuItem.text) === String(preSelectedItem) || String(menuItem.value) === String(preSelectedItem),
94
+ );
95
+
96
+ if (item) {
97
+ selectedItems.value.push(item);
98
+ }
99
+ });
100
+ }
101
+ };
102
+
103
+ const getListItemClasses = (item: SelectedItem) => ({
104
+ [listClasses.value.listItemClasses]: true,
105
+ 'spr-background-color-single-active': isItemSelected(item),
106
+ });
107
+
108
+ const handleSelectedItem = (item: SelectedItem) => {
109
+ if (!multiSelect.value) {
110
+ selectedItems.value = [item];
111
+
112
+ emit('get-selected-item', selectedItems.value[0]);
113
+ } else {
114
+ const index = selectedItems.value.findIndex((selectedItem: SelectedItem) => selectedItem.value === item.value);
115
+
116
+ if (index === -1) {
117
+ selectedItems.value.push(item);
118
+ } else {
119
+ selectedItems.value.splice(index, 1);
120
+ }
121
+
122
+ emit('get-selected-item', selectedItems.value);
123
+ }
124
+ };
125
+ // #endregion - Helper Methods
126
+
127
+ watch(
128
+ menuList,
129
+ () => {
130
+ setMenuList();
131
+ setPreSelectedItems();
132
+ },
133
+ { immediate: true },
134
+ );
135
+
136
+ onMounted(() => {
137
+ setMenuList();
138
+ setPreSelectedItems();
139
+ });
140
+
141
+ return {
142
+ listClasses,
143
+ initialMenuList,
144
+ groupedMenuList,
145
+ isItemSelected,
146
+ getListItemClasses,
147
+ handleSelectedItem,
148
+ };
149
+ };
@@ -5,16 +5,19 @@ export const definePropType = <T>(val: unknown): PropType<T> => val as PropType<
5
5
  const MODAL_SIZE = ['sm', 'md', 'lg', 'xl'] as const;
6
6
 
7
7
  export const modalPropTypes = {
8
- open: {
8
+ modelValue: {
9
9
  type: Boolean,
10
10
  default: false,
11
11
  },
12
-
13
12
  title: {
14
13
  type: String,
15
14
  default: '',
16
15
  },
17
- hasHeader: {
16
+ closeButtonX: {
17
+ type: Boolean,
18
+ default: true,
19
+ },
20
+ contentPadding: {
18
21
  type: Boolean,
19
22
  default: true,
20
23
  },
@@ -25,16 +28,18 @@ export const modalPropTypes = {
25
28
  size: {
26
29
  type: String as PropType<(typeof MODAL_SIZE)[number]>,
27
30
  validator: (value: (typeof MODAL_SIZE)[number]) => MODAL_SIZE.includes(value),
28
- default: 'sm',
31
+ default: 'md',
29
32
  },
30
- hasClose: {
33
+ staticBackdrop: {
31
34
  type: Boolean,
32
- default: true,
35
+ default: false,
33
36
  },
34
37
  };
35
38
 
36
39
  export type ModalPropTypes = ExtractPropTypes<typeof modalPropTypes>;
40
+
37
41
  export const modalEmitTypes = {
38
- onClose: () => true,
42
+ 'update:modelValue': (value: boolean) => value,
39
43
  };
44
+
40
45
  export type ModalEmitTypes = typeof modalEmitTypes;
@@ -1,35 +1,29 @@
1
1
  <template>
2
- <dialog ref="dialog" :class="[modalClasses.baseClasses, modalClasses.sizeClasses]">
3
- <header
4
- v-if="hasHeader"
5
- :class="[
6
- 'spr-border-color-weak spr-background-color spr-flex spr-items-center spr-justify-between spr-gap-size-spacing-3xs spr-border-x-0 spr-border-b spr-border-t-0 spr-border-solid spr-px-size-spacing-xs spr-py-size-spacing-2xs',
7
- 'spr-text-color-strong spr-subheading-xs',
8
- ]"
9
- >
10
- <slot name="header" />
11
-
12
- <div v-if="!$slots.header">{{ title }}</div>
13
-
14
- <span v-if="hasClose" class="spr-text-color-weak spr-subheading-xs spr-cursor-pointer" @click="closeModal">
15
- <Icon icon="ph:x" />
16
- </span>
17
- </header>
18
-
19
- <div class="spr-body-sm-regular">
20
- <slot />
21
- </div>
2
+ <transition name="backdrop-transition">
3
+ <div v-if="props.modelValue" :class="modalClasses.backdropClasses" @click="handleBackdropClick"></div>
4
+ </transition>
5
+
6
+ <transition name="modal-transition">
7
+ <div v-if="props.modelValue" id="modal" :class="modalClasses.baseClasses">
8
+ <header v-if="$slots.header || title" :class="modalClasses.headerClasses">
9
+ <span v-if="!$slots.header && title">{{ title }}</span>
10
+
11
+ <slot name="header"></slot>
12
+
13
+ <span v-if="props.closeButtonX" :class="modalClasses.headerCloseButtonXClasses" @click="handleCloseModal">
14
+ <Icon icon="ph:x" />
15
+ </span>
16
+ </header>
17
+
18
+ <div :class="modalClasses.contentClasses">
19
+ <slot />
20
+ </div>
22
21
 
23
- <footer
24
- v-if="hasFooter"
25
- :class="[
26
- 'spr-border-color-weak spr-background-color spr-flex spr-w-full spr-items-center spr-border-x-0 spr-border-b-0 spr-border-t spr-border-solid spr-px-size-spacing-xs spr-py-size-spacing-2xs',
27
- 'spr-text-color-strong spr-subheading-xs',
28
- ]"
29
- >
30
- <slot name="footer" />
31
- </footer>
32
- </dialog>
22
+ <footer v-if="$slots.footer" :class="modalClasses.footerClasses">
23
+ <slot name="footer" />
24
+ </footer>
25
+ </div>
26
+ </transition>
33
27
  </template>
34
28
 
35
29
  <script lang="ts" setup>
@@ -41,12 +35,58 @@ import { useModal } from './use-modal';
41
35
  const props = defineProps(modalPropTypes);
42
36
  const emit = defineEmits(modalEmitTypes);
43
37
 
44
- const { modalClasses, dialog, closeModal } = useModal(props, emit);
38
+ const { modalClasses, handleCloseModal, handleBackdropClick } = useModal(props, emit);
45
39
  </script>
46
40
 
47
41
  <style scoped>
48
- /* Tailwind CSS does not directly support the ::backdrop pseudo-element, need to add this */
49
- .spr-modal::backdrop {
50
- @apply spr-bg-overlay;
42
+ .backdrop-transition-enter-active,
43
+ .backdrop-transition-leave-active {
44
+ transition: opacity 150ms ease-in-out;
45
+ }
46
+
47
+ .backdrop-transition-enter-from,
48
+ .backdrop-transition-leave-to {
49
+ opacity: 0;
50
+ }
51
+
52
+ .modal-transition-enter-active,
53
+ .modal-transition-leave-active {
54
+ transition:
55
+ opacity 150ms ease-in-out,
56
+ transform 150ms ease-in-out;
57
+ }
58
+
59
+ .modal-transition-enter-from,
60
+ .modal-transition-leave-to {
61
+ opacity: 0;
62
+ transform: translateX(-50%) translateY(-50%) scale(0.9);
63
+ }
64
+
65
+ .modal-transition-enter-to,
66
+ .modal-transition-leave-from {
67
+ opacity: 1;
68
+ transform: translateX(-50%) translateY(-50%) scale(1);
69
+ }
70
+
71
+ .bounce-animation {
72
+ animation: bounce 0.5s ease;
73
+ }
74
+
75
+ @keyframes bounce {
76
+ 0% {
77
+ transform: translateX(-50%) translateY(-50%) scale(1);
78
+ }
79
+ 30% {
80
+ transform: translateX(-50%) translateY(-50%) scale(1.02);
81
+ }
82
+ 50% {
83
+ transform: translateX(-50%) translateY(-50%) scale(0.98);
84
+ }
85
+ 70% {
86
+ transform: translateX(-50%) translateY(-50%) scale(1.02);
87
+ }
88
+ 100% {
89
+ transform: translateX(-50%) translateY(-50%) scale(1);
90
+ }
51
91
  }
52
92
  </style>
@@ -1,4 +1,4 @@
1
- import { ref, toRefs, computed, ComputedRef, watch } from 'vue';
1
+ import { ref, toRefs, computed, ComputedRef } from 'vue';
2
2
 
3
3
  import classNames from 'classnames';
4
4
 
@@ -6,62 +6,96 @@ import type { SetupContext } from 'vue';
6
6
  import type { ModalEmitTypes, ModalPropTypes } from './modal';
7
7
 
8
8
  interface ModalClasses {
9
+ backdropClasses: string;
9
10
  baseClasses: string;
10
- sizeClasses: string;
11
+ headerClasses: string;
12
+ headerCloseButtonXClasses: string;
13
+ contentClasses: string;
14
+ footerClasses: string;
11
15
  }
12
16
 
13
17
  export const useModal = (props: ModalPropTypes, emit: SetupContext<ModalEmitTypes>['emit']) => {
14
- const { size } = toRefs(props);
15
-
16
- const dialog = ref<HTMLDialogElement | null>(null);
18
+ const { size, contentPadding, staticBackdrop } = toRefs(props);
17
19
 
18
20
  const modalClasses: ComputedRef<ModalClasses> = computed(() => {
21
+ const backdropClasses = classNames(
22
+ 'spr-fixed spr-bottom-0 spr-left-0 spr-right-0 spr-top-0 spr-z-[9999999] spr-h-full spr-w-full spr-bg-[#4C5857] spr-opacity-60',
23
+ );
24
+
19
25
  const baseClasses = classNames(
20
- 'spr-modal spr-background-color spr-rounded-border-radius-xl spr-p-0 spr-drop-shadow-[0_2px_8px_-2px_rgba(38,43,43,0.20)]',
26
+ 'spr-fixed spr-z-[99999999] spr-left-1/2 spr-top-1/2 spr-transform -spr-translate-x-1/2 -spr-translate-y-1/2',
27
+ 'spr-background-color spr-rounded-xl',
21
28
  'spr-border spr-border-solid spr-border-color-weak',
29
+ 'sm:spr-w-[calc(100%-2rem)] sm:spr-max-w-[calc(100%-2rem)]',
30
+ {
31
+ 'spr-w-[360px] spr-max-w-[480px]': size.value === 'sm',
32
+ 'spr-w-[480px] spr-max-w-[720px]': size.value === 'md',
33
+ 'spr-w-[720px] spr-max-w-[960px]': size.value === 'lg',
34
+ 'spr-w-[900px] spr-max-w-[1200px]': size.value === 'xl',
35
+ },
36
+ { 'bounce-animation': staticBackdropClicked.value },
37
+ );
38
+
39
+ const headerClasses = classNames(
40
+ 'spr-flex spr-items-start spr-justify-between spr-gap-2 spr-px-4 spr-py-3',
41
+ 'spr-text-color-strong spr-subheading-xs',
42
+ 'spr-border-color-weak spr-border-x-0 spr-border-b spr-border-t-0 spr-border-solid',
43
+ 'spr-rounded-tl-xl spr-rounded-tr-xl',
44
+ 'sm:spr-p-2',
45
+ );
46
+
47
+ const headerCloseButtonXClasses = classNames(
48
+ 'spr-text-color-weak spr-subheading-xs spr-mt-0.5 spr-cursor-pointer',
49
+ 'spr-transition spr-duration-150 spr-ease-in-out',
50
+ 'hover:spr-text-color-base',
51
+ 'active:spr-text-color-strong active:spr-scale-75',
22
52
  );
23
53
 
24
- const sizeClasses = classNames({
25
- 'spr-min-w-[360px] spr-max-w-[800px] ': size.value === 'sm',
26
- 'spr-min-w-[480px] spr-max-w-[800px] ': size.value === 'md',
27
- 'spr-w-[720px]': size.value === 'lg',
28
- 'spr-w-[1280px]': size.value === 'xl',
29
- });
54
+ const contentClasses = classNames(
55
+ 'spr-body-sm-regular spr-max-h-[calc(100vh-100px)] spr-overflow-y-auto spr-overflow-x-hidden',
56
+ {
57
+ 'spr-p-4 sm:spr-p-2': contentPadding.value,
58
+ },
59
+ );
60
+
61
+ const footerClasses = classNames(
62
+ 'spr-border-color-weak spr-border-x-0 spr-border-b-0 spr-border-t spr-border-solid',
63
+ 'spr-flex spr-w-full spr-items-center spr-px-size-spacing-xs spr-py-size-spacing-2xs',
64
+ 'spr-text-color-strong spr-subheading-xs',
65
+ );
30
66
 
31
67
  return {
68
+ backdropClasses,
32
69
  baseClasses,
33
- sizeClasses,
70
+ headerClasses,
71
+ headerCloseButtonXClasses,
72
+ contentClasses,
73
+ footerClasses,
34
74
  };
35
75
  });
36
76
 
37
- const openModal = () => {
38
- if (dialog.value) {
39
- dialog.value.showModal();
40
- }
77
+ const staticBackdropClicked = ref(false);
78
+
79
+ const handleCloseModal = () => {
80
+ emit('update:modelValue', false);
41
81
  };
42
82
 
43
- const closeModal = () => {
44
- if (dialog.value) {
45
- dialog.value.close();
46
- emit('onClose');
83
+ const handleBackdropClick = () => {
84
+ if (!staticBackdrop.value) {
85
+ handleCloseModal();
86
+ } else {
87
+ staticBackdropClicked.value = true;
88
+
89
+ setTimeout(() => {
90
+ staticBackdropClicked.value = false;
91
+ }, 500);
47
92
  }
48
93
  };
49
94
 
50
- watch(
51
- () => props.open,
52
- (value) => {
53
- if (value) {
54
- openModal();
55
- } else {
56
- closeModal();
57
- }
58
- },
59
- );
60
-
61
95
  return {
62
96
  modalClasses,
63
- dialog,
64
- openModal,
65
- closeModal,
97
+ staticBackdropClicked,
98
+ handleCloseModal,
99
+ handleBackdropClick,
66
100
  };
67
101
  };