design-system-next 2.15.7 → 2.16.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,134 @@
1
+ import type { PropType, ExtractPropTypes } from 'vue';
2
+ import type { MenuListType } from '../list/list';
3
+
4
+ export const definePropType = <T>(val: unknown): PropType<T> => val as PropType<T>;
5
+ const TRIGGER_EVENTS = ['click', 'hover', 'focus', 'touch'] as const;
6
+ const POPPER_STRATEGY_TYPES = ['fixed', 'absolute'] as const;
7
+ const PLACEMENTS_TYPES = [
8
+ 'auto',
9
+ 'auto-start',
10
+ 'auto-end',
11
+ 'top',
12
+ 'top-start',
13
+ 'top-end',
14
+ 'right',
15
+ 'right-start',
16
+ 'right-end',
17
+ 'bottom',
18
+ 'bottom-start',
19
+ 'bottom-end',
20
+ 'left',
21
+ 'left-start',
22
+ 'left-end',
23
+ ] as const;
24
+
25
+ export const attributeFilterPropTypes = {
26
+ id: {
27
+ type: String,
28
+ default: 'attribute_filter',
29
+ },
30
+ filterLabel: {
31
+ type: String,
32
+ default: 'Filter',
33
+ },
34
+ headerLabel: {
35
+ type: String,
36
+ default: 'Add Filter',
37
+ },
38
+ triggers: {
39
+ type: Array as PropType<(typeof TRIGGER_EVENTS)[number][]>,
40
+ validator: (value: (typeof TRIGGER_EVENTS)[number][]) => {
41
+ return value.every((val) => TRIGGER_EVENTS.includes(val));
42
+ },
43
+ default: () => [],
44
+ },
45
+ popperTriggers: {
46
+ type: Array as PropType<(typeof TRIGGER_EVENTS)[number][]>,
47
+ validator: (value: (typeof TRIGGER_EVENTS)[number][]) => {
48
+ return value.every((val) => TRIGGER_EVENTS.includes(val));
49
+ },
50
+ default: () => [],
51
+ },
52
+ autoHide: {
53
+ type: Boolean,
54
+ default: true,
55
+ },
56
+ popperStrategy: {
57
+ type: String,
58
+ validator: (value: 'fixed' | 'absolute') => POPPER_STRATEGY_TYPES.includes(value),
59
+ default: 'absolute',
60
+ },
61
+ distance: {
62
+ type: Number,
63
+ default: 6,
64
+ },
65
+ placement: {
66
+ type: String as PropType<(typeof PLACEMENTS_TYPES)[number]>,
67
+ validator: (value: (typeof PLACEMENTS_TYPES)[number]) => PLACEMENTS_TYPES.includes(value),
68
+ default: 'bottom',
69
+ },
70
+ disabled: {
71
+ type: Boolean,
72
+ default: false,
73
+ },
74
+ wrapperPosition: {
75
+ type: String,
76
+ default: 'relative',
77
+ },
78
+ width: {
79
+ type: String,
80
+ default: '100%',
81
+ },
82
+ popperWidth: {
83
+ type: String,
84
+ default: '100%',
85
+ },
86
+ popperInnerWidth: {
87
+ type: String,
88
+ default: 'unset',
89
+ },
90
+ searchable: {
91
+ type: Boolean,
92
+ default: false,
93
+ },
94
+ multiselect: {
95
+ type: Boolean,
96
+ default: false,
97
+ },
98
+ filterMenuList: {
99
+ type: Array as PropType<MenuListType[] | string[]>,
100
+ default: [],
101
+ },
102
+ disableLocalSearch: {
103
+ type: Boolean,
104
+ default: false,
105
+ },
106
+ showBadge: {
107
+ type: Boolean,
108
+ default: true,
109
+ },
110
+ badgeVariant: {
111
+ type: String,
112
+ default: 'danger',
113
+ },
114
+ noList: {
115
+ type: Boolean,
116
+ default: false,
117
+ },
118
+ clearable: {
119
+ type: Boolean,
120
+ default: true,
121
+ }
122
+ };
123
+
124
+ export const attributeFilterEmitTypes = {
125
+ onSaveFilter: (savedFilters: MenuListType[]) => Array.isArray(savedFilters),
126
+ onCloseFilter: () => true,
127
+ onOpenFilter: () => true,
128
+ onSelectFilter: (selectedFilters: MenuListType[]) => Array.isArray(selectedFilters),
129
+ infiniteScrollTrigger: () => true,
130
+ onClearFilter: () => true,
131
+ };
132
+
133
+ export type AttributeFilterPropTypes = ExtractPropTypes<typeof attributeFilterPropTypes>;
134
+ export type AttributeFilterEmitTypes = typeof attributeFilterEmitTypes;
@@ -0,0 +1,141 @@
1
+ <template>
2
+ <Menu
3
+ :shown="isFilterActive"
4
+ aria-id="attribute_filter_wrapper"
5
+ :distance="props.distance"
6
+ :placement="props.placement"
7
+ :triggers="props.triggers"
8
+ :popper-triggers="props.popperTriggers"
9
+ :auto-hide="props.autoHide"
10
+ :disabled="props.disabled"
11
+ :container="`#${props.id}`"
12
+ :strategy="
13
+ props.popperStrategy === 'fixed' || props.popperStrategy === 'absolute' ? props.popperStrategy : 'absolute'
14
+ "
15
+ :delay="0"
16
+ :style="{
17
+ position: props.wrapperPosition,
18
+ width: props.width,
19
+ }"
20
+ @hide="handleClosePopper"
21
+ @show="handleShowPopper"
22
+ >
23
+ <div @click="handleFilterTrigger">
24
+ <slot>
25
+ <spr-chips
26
+ :label="props.filterLabel"
27
+ :active="isFilterActive"
28
+ icon="ph:funnel-simple"
29
+ :badge="props.showBadge && savedFilters.length > 0"
30
+ :badge-text="savedFilters.length.toString()"
31
+ :badge-variant="props.badgeVariant"
32
+ :closable="props.clearable && savedFilters.length > 0"
33
+ :disabled="props.disabled"
34
+ @close="handleClear"
35
+ />
36
+ </slot>
37
+ </div>
38
+
39
+ <div
40
+ :id="props.id"
41
+ :style="{
42
+ width: props.popperWidth,
43
+ }"
44
+ ></div>
45
+ <template #popper="{ hide }">
46
+ <div
47
+ id="attribute_filter_popper"
48
+ :style="{
49
+ width: props.popperInnerWidth,
50
+ }"
51
+ >
52
+ <slot name="header">
53
+ <div
54
+ id="attribute_filter_header"
55
+ class="spr-border-color-weak spr-flex spr-items-center spr-justify-between spr-border-x-0 spr-border-b spr-border-t-0 spr-border-solid spr-px-size-spacing-xs spr-py-size-spacing-2xs"
56
+ >
57
+ <span class="spr-text-color-strong spr-text-300 spr-font-medium"> {{ props.headerLabel }} </span>
58
+ <Icon
59
+ icon="ph:x"
60
+ width="20px"
61
+ height="20px"
62
+ class="spr-text-color-weak spr-cursor-pointer"
63
+ @click="hide()"
64
+ />
65
+ </div>
66
+ </slot>
67
+ <div
68
+ v-if="props.searchable"
69
+ id="attribute_filter_subheader"
70
+ class="spr-border-color-weak spr-border-x-0 spr-border-b spr-border-t-0 spr-border-solid spr-px-size-spacing-xs spr-py-size-spacing-2xs"
71
+ >
72
+ <spr-input-search
73
+ v-model="searchModel"
74
+ label=""
75
+ placeholder="Search..."
76
+ class="!spr-py-0"
77
+ @focus="isSearchFocused = true"
78
+ @blur="isSearchFocused = false"
79
+ />
80
+ </div>
81
+ <slot name="actions"> </slot>
82
+ <slot name="body">
83
+ <div
84
+ v-if="!noList"
85
+ ref="filterDropdownRef"
86
+ id="attribute_filter_body"
87
+ class="spr-max-h-[250px] spr-overflow-y-auto"
88
+ >
89
+ <spr-list
90
+ v-model="selectedFilters"
91
+ :menu-list="attributeFilterList"
92
+ :multi-select="props.multiselect"
93
+ @update:model-value="handleOnSelect"
94
+ />
95
+ </div>
96
+ </slot>
97
+ <slot name="footer">
98
+ <div
99
+ id="attribute_filter_footer"
100
+ class="spr-border-color-weak spr-flex spr-items-center spr-justify-end spr-gap-size-spacing-3xs spr-border-x-0 spr-border-b-0 spr-border-t spr-border-solid spr-px-size-spacing-xs spr-py-size-spacing-2xs"
101
+ >
102
+ <spr-button variant="secondary" size="medium" @click="isFilterActive = false"> Cancel </spr-button>
103
+ <spr-button variant="primary" size="medium" tone="success" @click="handleSave"> Save </spr-button>
104
+ </div>
105
+ </slot>
106
+ </div>
107
+ </template>
108
+ </Menu>
109
+ </template>
110
+ <script lang="ts" setup>
111
+ import { Menu } from 'floating-vue';
112
+ import { attributeFilterEmitTypes, attributeFilterPropTypes } from './attribute-filter';
113
+ import { useAttributeFilter } from './use-attribute-filter';
114
+ import SprChips from '../chips/chips.vue';
115
+ import { Icon } from '@iconify/vue';
116
+ import SprInputSearch from '../input/input-search/input-search.vue';
117
+ import SprList from '../list/list.vue';
118
+ import SprButton from '../button/button.vue';
119
+
120
+ import 'floating-vue/dist/style.css';
121
+
122
+ const props = defineProps(attributeFilterPropTypes);
123
+ const emit = defineEmits(attributeFilterEmitTypes);
124
+ const searchModel = defineModel<string>('search', {
125
+ default: '',
126
+ });
127
+ const {
128
+ isFilterActive,
129
+ isSearchFocused,
130
+ attributeFilterList,
131
+ handleFilterTrigger,
132
+ selectedFilters,
133
+ savedFilters,
134
+ handleClosePopper,
135
+ handleShowPopper,
136
+ handleSave,
137
+ handleOnSelect,
138
+ filterDropdownRef,
139
+ handleClear,
140
+ } = useAttributeFilter(props, emit, searchModel);
141
+ </script>
@@ -0,0 +1,125 @@
1
+ import type { MenuListType } from '../list/list';
2
+ import type { AttributeFilterPropTypes, AttributeFilterEmitTypes } from './attribute-filter';
3
+ import { onMounted, ref, toRefs, watch, type SetupContext, type Ref } from 'vue';
4
+ import { useInfiniteScroll } from '@vueuse/core';
5
+ export const useAttributeFilter = (
6
+ props: AttributeFilterPropTypes,
7
+ emit: SetupContext<AttributeFilterEmitTypes>['emit'],
8
+ searchModel: Ref<string>,
9
+ ) => {
10
+ const isFilterActive = ref(false);
11
+ const isSearchFocused = ref(false);
12
+ const selectedFilters = ref<MenuListType[]>([]);
13
+ const savedFilters = ref<MenuListType[]>([]);
14
+ const isSaving = ref(false);
15
+ const filterDropdownRef = ref<HTMLDivElement | null>(null);
16
+
17
+ const { filterMenuList, disableLocalSearch, noList } = toRefs(props);
18
+ const attributeFilterList = ref<MenuListType[]>([]);
19
+
20
+ const handleFilterTrigger = () => {
21
+ isFilterActive.value = !isFilterActive.value;
22
+ };
23
+
24
+ const getFilterList = () => {
25
+ if (Array.isArray(filterMenuList.value)) {
26
+ attributeFilterList.value = filterMenuList.value as MenuListType[];
27
+ }
28
+
29
+ if (typeof filterMenuList.value[0] === 'string' || typeof attributeFilterList.value[0] === 'number') {
30
+ attributeFilterList.value = (filterMenuList.value as string[] | number[]).map((item) => ({
31
+ text: item.toString(),
32
+ value: item,
33
+ }));
34
+ }
35
+ };
36
+
37
+ const handleClosePopper = () => {
38
+ if (isSaving.value) {
39
+ isSaving.value = false;
40
+ return;
41
+ }
42
+
43
+ isFilterActive.value = false;
44
+ emit('onCloseFilter');
45
+ };
46
+
47
+ const handleShowPopper = () => {
48
+ emit('onOpenFilter');
49
+ if (noList.value) return;
50
+
51
+ selectedFilters.value = [...savedFilters.value];
52
+ };
53
+
54
+ const handleSave = () => {
55
+ isSaving.value = true;
56
+ savedFilters.value = [...selectedFilters.value];
57
+ isFilterActive.value = false;
58
+
59
+ emit('onSaveFilter', savedFilters.value);
60
+ };
61
+
62
+ const getFilteredMenuList = (list: MenuListType[]) => {
63
+ return list.filter((item: MenuListType) => {
64
+ const searchTerm = searchModel.value.toLowerCase().trim();
65
+ return item.text.toLowerCase().trim().includes(searchTerm);
66
+ });
67
+ };
68
+
69
+ const handleSearch = () => {
70
+ if (disableLocalSearch.value || noList.value) return;
71
+ getFilterList();
72
+ attributeFilterList.value = getFilteredMenuList(attributeFilterList.value);
73
+ };
74
+
75
+ const handleOnSelect = (selectedFilters: MenuListType[]) => {
76
+ emit('onSelectFilter', selectedFilters);
77
+ };
78
+
79
+ const handleClear = () => {
80
+ selectedFilters.value = [];
81
+ savedFilters.value = [];
82
+ emit('onClearFilter');
83
+ };
84
+
85
+ useInfiniteScroll(
86
+ filterDropdownRef,
87
+ () => {
88
+ if (!filterDropdownRef.value) return;
89
+ const element = filterDropdownRef.value;
90
+ const hasVerticalScrollbar = element.scrollHeight > element.clientHeight;
91
+
92
+ if (hasVerticalScrollbar) {
93
+ emit('infiniteScrollTrigger');
94
+ }
95
+ },
96
+ { distance: 10 },
97
+ );
98
+
99
+ onMounted(() => {
100
+ if (noList.value) return;
101
+ getFilterList();
102
+ });
103
+
104
+ watch(filterMenuList, () => {
105
+ if (noList.value) return;
106
+ getFilterList();
107
+ });
108
+
109
+ watch(searchModel, () => handleSearch());
110
+
111
+ return {
112
+ isFilterActive,
113
+ isSearchFocused,
114
+ attributeFilterList,
115
+ handleFilterTrigger,
116
+ selectedFilters,
117
+ handleClosePopper,
118
+ handleShowPopper,
119
+ handleSave,
120
+ savedFilters,
121
+ handleOnSelect,
122
+ filterDropdownRef,
123
+ handleClear,
124
+ };
125
+ };
@@ -2,9 +2,17 @@ import type { PropType, ExtractPropTypes } from 'vue';
2
2
 
3
3
  export const definePropType = <T>(val: unknown): PropType<T> => val as PropType<T>;
4
4
 
5
+ const TONES = ['plain', 'neutral', 'success', 'information', 'pending', 'caution', 'accent', 'danger'] as const;
5
6
  const BORDER_RADIUS_SIZE = ['xl', 'lg', 'md', 'sm', 'xs', '2xs'] as const;
6
7
 
7
8
  export const cardPropTypes = {
9
+ id: {
10
+ type: String,
11
+ },
12
+ tone: {
13
+ type: String as PropType<(typeof TONES)[number] | undefined>,
14
+ validator: (val: string | undefined) => !val || TONES.includes(val as (typeof TONES)[number]),
15
+ },
8
16
  title: {
9
17
  type: String,
10
18
  default: '',
@@ -30,6 +38,10 @@ export const cardPropTypes = {
30
38
  validator: (value: (typeof BORDER_RADIUS_SIZE)[number]) => BORDER_RADIUS_SIZE.includes(value),
31
39
  default: 'xl',
32
40
  },
41
+ borderWidth: {
42
+ type: String,
43
+ default: '1px',
44
+ },
33
45
  hasCollapsible: {
34
46
  type: Boolean,
35
47
  default: false,
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="cardClasses.baseClasses">
2
+ <div :class="cardClasses.baseClasses" :style="{ borderWidth: props.borderWidth }">
3
3
  <div v-if="props.showHeader && ($slots.header || props.title)" :class="cardClasses.headerClasses">
4
4
  <div v-if="props.title" class="spr-flex spr-items-center">
5
5
  <Icon
@@ -12,21 +12,42 @@ interface CardClasses {
12
12
  }
13
13
 
14
14
  export const useCard = (props: CardPropTypes, slots: Slots) => {
15
- const { title, headerIcon, borderRadiusSize, hasCollapsible, isCollapsibleOpen, hasContentPadding, flexbox } =
15
+ const { tone, title, headerIcon, borderRadiusSize, hasCollapsible, isCollapsibleOpen, hasContentPadding, flexbox } =
16
16
  toRefs(props);
17
17
 
18
18
  const cardClasses: ComputedRef<CardClasses> = computed(() => {
19
- const baseClasses = classNames(
20
- `spr-bg-white-50 spr-text-color-strong spr-border spr-border-solid spr-border-mushroom-200`,
21
- {
22
- 'spr-rounded-border-radius-2xs': borderRadiusSize.value === 'xs',
23
- 'spr-rounded-border-radius-xs': borderRadiusSize.value === 'sm',
24
- 'spr-rounded-border-radius-sm': borderRadiusSize.value === 'md',
25
- 'spr-rounded-border-radius-md': borderRadiusSize.value === 'xs',
26
- 'spr-rounded-border-radius-lg': borderRadiusSize.value === 'lg',
27
- 'spr-rounded-border-radius-xl': borderRadiusSize.value === 'xl' || !borderRadiusSize.value,
28
- },
29
- );
19
+ const toneValue = tone?.value;
20
+
21
+ const baseClasses = classNames(`spr-border-solid`, {
22
+ // Tones
23
+ 'spr-background-color-base': !toneValue,
24
+ 'spr-bg-white': toneValue === 'plain',
25
+ 'spr-background-color-surface': toneValue === 'neutral',
26
+ 'spr-background-color-success-weak': toneValue === 'success',
27
+ 'spr-background-color-information-weak': toneValue === 'information',
28
+ 'spr-background-color-pending-weak': toneValue === 'pending',
29
+ 'spr-background-color-caution-weak': toneValue === 'caution',
30
+ 'spr-background-color-accent-weak': toneValue === 'accent',
31
+ 'spr-background-color-danger-weak': toneValue === 'danger',
32
+
33
+ // Borders
34
+ 'spr-border-color-weak': !toneValue || toneValue === 'plain',
35
+ 'spr-border-color-base': toneValue === 'neutral',
36
+ 'spr-border-color-success-base': toneValue === 'success',
37
+ 'spr-border-color-information-base': toneValue === 'information',
38
+ 'spr-border-color-pending-base': toneValue === 'pending',
39
+ 'spr-border-color-caution-base': toneValue === 'caution',
40
+ 'spr-border-color-accent-base': toneValue === 'accent',
41
+ 'spr-border-color-danger-base': toneValue === 'danger',
42
+
43
+ // Border radius
44
+ 'spr-rounded-border-radius-2xs': borderRadiusSize.value === 'xs',
45
+ 'spr-rounded-border-radius-xs': borderRadiusSize.value === 'sm',
46
+ 'spr-rounded-border-radius-sm': borderRadiusSize.value === 'md',
47
+ 'spr-rounded-border-radius-md': borderRadiusSize.value === 'xs',
48
+ 'spr-rounded-border-radius-lg': borderRadiusSize.value === 'lg',
49
+ 'spr-rounded-border-radius-xl': borderRadiusSize.value === 'xl' || !borderRadiusSize.value,
50
+ });
30
51
 
31
52
  const headerClasses = classNames(`spr-flex spr-items-center transition-all duration-300 ease-in-out`, {
32
53
  'spr-min-h-[18px]': slots.header,
@@ -39,8 +39,7 @@ export const dropdownPropTypes = {
39
39
  },
40
40
  menuList: {
41
41
  type: Array as PropType<MenuListType[] | string[] | Record<string, unknown>[]>,
42
- required: true,
43
- default: [],
42
+ default: () => [],
44
43
  },
45
44
  searchableMenu: {
46
45
  type: Boolean,
@@ -116,10 +115,6 @@ export const dropdownPropTypes = {
116
115
  type: Boolean,
117
116
  default: false,
118
117
  },
119
- dropdown: {
120
- type: Boolean,
121
- default: false,
122
- },
123
118
  lozenge: {
124
119
  type: Boolean,
125
120
  default: false,
@@ -30,47 +30,57 @@
30
30
  ></div>
31
31
 
32
32
  <template #popper>
33
- <div
34
- ref="dropdownRef"
35
- :class="[
36
- 'spr-grid spr-max-h-[300px] spr-gap-0.5 spr-overflow-y-auto spr-overflow-x-hidden',
37
- !props.ladderized || isLadderizedSearch,
38
- ]"
39
- :style="{
40
- width: props.popperInnerWidth,
41
- }"
42
- >
43
- <template v-if="dropdownMenuList.length > 0">
44
- <spr-list
45
- v-if="!props.ladderized || isLadderizedSearch"
46
- v-model="selectedListItems"
47
- :menu-list="dropdownMenuList"
48
- :searchable-menu="props.searchableMenu"
49
- :group-items-by="props.groupItemsBy"
50
- :multi-select="props.multiSelect"
51
- :pre-selected-items="dropdownValue"
52
- :no-check="props.noCheckInList"
53
- :dropdown="props.dropdown"
54
- :lozenge="props.lozenge"
55
- @update:model-value="handleSelectedItem"
56
- />
57
- <spr-ladderized-list
58
- v-else
59
- v-model="dropdownValue"
60
- :ladderized="props.ladderized"
61
- :dropdown="props.dropdown"
62
- :menu-list="dropdownMenuList"
63
- :searchable-menu="props.searchableMenu"
64
- :remove-current-level-in-back-label="removeCurrentLevelInBackLabel"
65
- @update:model-value="handleSelectedLadderizedItem"
66
- />
67
- </template>
68
- <template v-else>
69
- <div class="spr-flex spr-items-center spr-justify-center spr-p-2 spr-text-center">
70
- <span class="spr-body-sm-regular spr-m-0">No results found</span>
71
- </div>
72
- </template>
73
- </div>
33
+ <template v-if="$slots.popper">
34
+ <div
35
+ class="spr-overflow-y-auto spr-overflow-x-hidden spr-p-2"
36
+ :style="{
37
+ width: props.popperInnerWidth,
38
+ }"
39
+ >
40
+ <slot name="popper" />
41
+ </div>
42
+ </template>
43
+ <template v-else>
44
+ <div
45
+ ref="dropdownRef"
46
+ :class="[
47
+ 'spr-grid spr-max-h-[300px] spr-gap-0.5 spr-overflow-y-auto spr-overflow-x-hidden',
48
+ !props.ladderized || isLadderizedSearch,
49
+ ]"
50
+ :style="{
51
+ width: props.popperInnerWidth,
52
+ }"
53
+ >
54
+ <template v-if="dropdownMenuList.length > 0">
55
+ <spr-list
56
+ v-if="!props.ladderized || isLadderizedSearch"
57
+ v-model="selectedListItems"
58
+ :menu-list="dropdownMenuList"
59
+ :searchable-menu="props.searchableMenu"
60
+ :group-items-by="props.groupItemsBy"
61
+ :multi-select="props.multiSelect"
62
+ :pre-selected-items="dropdownValue"
63
+ :no-check="props.noCheckInList"
64
+ :lozenge="props.lozenge"
65
+ @update:model-value="handleSelectedItem"
66
+ />
67
+ <spr-ladderized-list
68
+ v-else
69
+ v-model="dropdownValue"
70
+ :ladderized="props.ladderized"
71
+ :menu-list="dropdownMenuList"
72
+ :searchable-menu="props.searchableMenu"
73
+ :remove-current-level-in-back-label="removeCurrentLevelInBackLabel"
74
+ @update:model-value="handleSelectedLadderizedItem"
75
+ />
76
+ </template>
77
+ <template v-else>
78
+ <div class="spr-flex spr-items-center spr-justify-center spr-p-2 spr-text-center">
79
+ <span class="spr-body-sm-regular spr-m-0">No results found</span>
80
+ </div>
81
+ </template>
82
+ </div>
83
+ </template>
74
84
  </template>
75
85
  </Menu>
76
86
  </template>
@@ -82,10 +82,6 @@ export const listPropTypes = {
82
82
  type: Boolean,
83
83
  default: false,
84
84
  },
85
- dropdown: {
86
- type: Boolean,
87
- default: false,
88
- },
89
85
  lozenge: {
90
86
  type: Boolean,
91
87
  default: false,
@@ -42,13 +42,16 @@
42
42
  :error="props.error"
43
43
  >
44
44
  <template #icon>
45
- <div class="spr-flex spr-items-center spr-gap-1">
46
- <Icon
47
- v-if="props.clearable && inputText"
48
- class="spr-cursor-pointer"
49
- icon="ph:x"
50
- @click.stop="handleClear"
51
- />
45
+ <div
46
+ :class="[
47
+ 'spr-flex spr-items-center spr-gap-1',
48
+ {
49
+ 'spr-cursor-pointer': !props.disabled,
50
+ 'spr-cursor-not-allowed': props.disabled,
51
+ },
52
+ ]"
53
+ >
54
+ <Icon v-if="props.clearable && inputText" icon="ph:x" @click.stop="handleClear" />
52
55
  <Icon icon="ph:caret-down" />
53
56
  </div>
54
57
  </template>
@@ -147,6 +147,8 @@ export const useSelectLadderized = (
147
147
  };
148
148
 
149
149
  const handleClear = () => {
150
+ if (disabled.value) return;
151
+
150
152
  wasCleared.value = true;
151
153
 
152
154
  inputText.value = '';