design-system-next 2.26.3 → 2.26.7

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.
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "design-system-next",
3
3
  "private": false,
4
- "version": "2.26.3",
4
+ "version": "2.26.7",
5
5
  "main": "./dist/design-system-next.umd.js",
6
6
  "module": "./dist/design-system-next.es.js",
7
7
  "types": "./dist/design-system-next.es.d.ts",
@@ -46,7 +46,6 @@
46
46
  "docs:preview": "vitepress preview docs",
47
47
  "test:ui": "npx playwright test --ui --config=playwright-ct.config.ts",
48
48
  "test:components": "npx playwright test --config=playwright-ct.config.ts --reporter=line",
49
- "test:component": "npx playwright test --config=playwright-ct.config.ts --reporter=line",
50
49
  "clean": "rm -rf node_modules dist coverage package-lock.json && npm install"
51
50
  },
52
51
  "dependencies": {
@@ -15,7 +15,10 @@
15
15
  <div class="spr-heading-xs">{{ weekRangeDisplay }}</div>
16
16
  </div>
17
17
 
18
- <spr-button id="calendar-today" variant="secondary" size="large" @click="goToToday"> Today </spr-button>
18
+ <div class="spr-flex spr-items-center spr-justify-center spr-gap-size-spacing-3xs">
19
+ <spr-button id="calendar-today" variant="secondary" size="large" @click="goToToday"> Today </spr-button>
20
+ <slot name="headerActions" />
21
+ </div>
19
22
  </div>
20
23
  <!-- Filters -->
21
24
  <slot name="filter" />
@@ -159,9 +162,20 @@
159
162
  </div>
160
163
  </section>
161
164
 
165
+ <section>
166
+ <slot
167
+ name="fixedCell"
168
+ :details="{
169
+ employeeId: employee.id,
170
+ date: formatDate(date, dateFormat),
171
+ shift: employee.schedule[formatDate(date, dateFormat)],
172
+ }"
173
+ />
174
+ </section>
175
+
162
176
  <section v-if="showCustomSlot(index, employee.id)">
163
177
  <slot
164
- name="cell"
178
+ name="hoverCell"
165
179
  :details="{
166
180
  employeeId: employee.id,
167
181
  date: formatDate(date, dateFormat),
@@ -119,7 +119,10 @@ export const useCalendar = (props: CalendarPropTypes, emit: SetupContext<Calenda
119
119
 
120
120
  const showCustomSlot = (index: number, employeeId: number) => {
121
121
  return (
122
- state.hoveredCell.value === index && state.isHover.value && state.employeeId.value === employeeId && slots.cell
122
+ state.hoveredCell.value === index &&
123
+ state.isHover.value &&
124
+ state.employeeId.value === employeeId &&
125
+ slots.hoverCell
123
126
  );
124
127
  };
125
128
 
@@ -1,6 +1,5 @@
1
1
  import type { PropType, ExtractPropTypes } from 'vue';
2
2
  import type { MenuListType } from '../list';
3
-
4
3
  export const listItemPropTypes = {
5
4
  item: {
6
5
  type: Object as PropType<MenuListType>,
@@ -50,6 +49,14 @@ export const listItemPropTypes = {
50
49
  type: Boolean,
51
50
  default: false,
52
51
  },
52
+ avatarVariant: {
53
+ type: String,
54
+ default: '',
55
+ },
56
+ avatarSource: {
57
+ type: String,
58
+ default: '',
59
+ },
53
60
  };
54
61
 
55
62
  export const listItemEmitTypes = {
@@ -46,6 +46,16 @@
46
46
  >
47
47
  <Icon class="spr-text-xl" :icon="iconName" />
48
48
  </span>
49
+
50
+ <span v-if="hasAvatar">
51
+ <spr-avatar
52
+ size="sm"
53
+ :initial="props.avatarSource || listItem.text"
54
+ :variant="props.avatarVariant"
55
+ :src="props.avatarSource"
56
+ />
57
+ </span>
58
+
49
59
  <div
50
60
  :class="[
51
61
  'spr-flex spr-flex-auto spr-flex-col spr-justify-start',
@@ -106,6 +116,7 @@ import { Icon } from '@iconify/vue';
106
116
  import SprCheckbox from '@/components/checkbox/checkbox.vue';
107
117
  import SprRadio from '@/components/radio/radio.vue';
108
118
  import SprLozenge from '@/components/lozenge/lozenge.vue';
119
+ import SprAvatar from '@/components/avatar/avatar.vue';
109
120
 
110
121
  import { LOZENGE_TONE } from '@/components/lozenge/lozenge';
111
122
 
@@ -115,5 +126,5 @@ import { useListItem } from './use-list-item';
115
126
  const props = defineProps(listItemPropTypes);
116
127
  const emit = defineEmits(listItemEmitTypes);
117
128
 
118
- const { listItem, hasIcon, iconName, iconClasses, hasSublevels, showLozengeMode } = useListItem(props, emit);
129
+ const { listItem, hasIcon, iconName, iconClasses, hasSublevels, showLozengeMode, hasAvatar } = useListItem(props, emit);
119
130
  </script>
@@ -14,13 +14,16 @@ export function useListItem(
14
14
  iconClasses: ComputedRef<string>;
15
15
  hasSublevels: ComputedRef<boolean>;
16
16
  showLozengeMode: ComputedRef<boolean>;
17
+ hasAvatar: ComputedRef<boolean>;
17
18
  } {
18
- const { item, itemIcon, itemIconTone, itemIconFill, lozenge } = toRefs(props);
19
+ const { item, itemIcon, itemIconTone, itemIconFill, lozenge, avatarVariant } = toRefs(props);
19
20
 
20
21
  const listItem = computed(() => item?.value);
21
22
 
22
23
  const hasIcon = computed(() => !!(itemIcon.value || item?.value!.icon));
23
24
 
25
+ const hasAvatar = computed(() => !!(avatarVariant.value && !hasIcon.value));
26
+
24
27
  const iconName = computed(() => itemIcon.value || item?.value!.icon || '');
25
28
 
26
29
  const iconClasses = computed(() => {
@@ -64,6 +67,7 @@ export function useListItem(
64
67
  return {
65
68
  listItem,
66
69
  hasIcon,
70
+ hasAvatar,
67
71
  iconName,
68
72
  iconClasses,
69
73
  hasSublevels,
@@ -125,8 +125,20 @@ export const listPropTypes = {
125
125
  },
126
126
  allowDeselect: {
127
127
  type: Boolean,
128
- default: false
129
- }
128
+ default: false,
129
+ },
130
+ allowSelectAll: {
131
+ type: Boolean,
132
+ default: false,
133
+ },
134
+ avatarVariant: {
135
+ type: String,
136
+ default: '',
137
+ },
138
+ avatarSource: {
139
+ type: String,
140
+ default: '',
141
+ },
130
142
  };
131
143
 
132
144
  export const listEmitTypes = {
@@ -1,7 +1,9 @@
1
1
  <template>
2
2
  <div class="spr-font-main">
3
3
  <!-- Header Section -->
4
- <template v-if="props.searchableMenu || props.displayListItemSelected">
4
+ <template
5
+ v-if="props.searchableMenu || props.displayListItemSelected || (props.multiSelect && props.allowSelectAll)"
6
+ >
5
7
  <div :class="listClasses.headerClasses" :style="stickyOffsetStyle">
6
8
  <spr-input-search
7
9
  v-if="props.searchableMenu"
@@ -10,12 +12,34 @@
10
12
  autocomplete="off"
11
13
  @keyup="handleSearchKeyup"
12
14
  />
13
- <span
14
- v-if="props.supportingDisplayText || props.displayListItemSelected"
15
- class="spr-label-sm-medium spr-text-color-base spr-block"
16
- >
17
- {{ props.supportingDisplayText || `${selectedItems.length} Selected` }}
18
- </span>
15
+ <slot name="list-controls">
16
+ <div
17
+ v-if="
18
+ props.supportingDisplayText ||
19
+ props.displayListItemSelected ||
20
+ (props.multiSelect && props.allowSelectAll)
21
+ "
22
+ :class="listClasses.listControlsClasses"
23
+ >
24
+ <span
25
+ v-if="props.supportingDisplayText || props.displayListItemSelected"
26
+ class="spr-label-sm-medium spr-text-color-base spr-block"
27
+ >
28
+ {{ props.supportingDisplayText || `${selectedItems.length} Selected` }}
29
+ </span>
30
+
31
+ <spr-button
32
+ v-if="props.multiSelect && props.allowSelectAll"
33
+ id="select-all-button"
34
+ :class="{ 'spr-ml-auto': true }"
35
+ variant="secondary"
36
+ size="small"
37
+ @click="handleSelectAll"
38
+ >
39
+ {{ hasSelectedItems ? 'Unselect All' : 'Select All' }}
40
+ </spr-button>
41
+ </div>
42
+ </slot>
19
43
  </div>
20
44
  </template>
21
45
 
@@ -46,6 +70,8 @@
46
70
  :item-icon-fill="props.itemIconFill"
47
71
  :disabled-unselected-items="props.disabledUnselectedItems"
48
72
  :radio-list="props.radioList"
73
+ :avatar-variant="props.avatarVariant"
74
+ :avatar-source="props.avatarSource"
49
75
  @select="handleSelectedItem(item)"
50
76
  />
51
77
  <div v-if="props.infiniteScrollLoader" class="spr-flex spr-items-center spr-justify-center spr-p-2">
@@ -74,6 +100,8 @@
74
100
  :item-icon-fill="props.itemIconFill"
75
101
  :disabled-unselected-items="props.disabledUnselectedItems"
76
102
  :radio-list="props.radioList"
103
+ :avatar-variant="props.avatarVariant"
104
+ :avatar-source="props.avatarSource"
77
105
  @select="handleSelectedItem(item)"
78
106
  />
79
107
  <div v-if="props.infiniteScrollLoader" class="spr-flex spr-items-center spr-justify-center spr-p-2">
@@ -103,6 +131,7 @@
103
131
  import { Icon } from '@iconify/vue';
104
132
 
105
133
  import SprInputSearch from '@/components/input/input-search/input-search.vue';
134
+ import SprButton from '@/components/button/button.vue';
106
135
  import ListItem from './list-item/list-item.vue';
107
136
 
108
137
  import { listPropTypes, listEmitTypes } from './list';
@@ -119,9 +148,11 @@ const {
119
148
  localizedMenuList,
120
149
  groupedMenuList,
121
150
  hasGroupedItems,
151
+ hasSelectedItems,
122
152
  isItemSelected,
123
153
  getListItemClasses,
124
154
  handleSelectedItem,
155
+ handleSelectAll,
125
156
  handleSearchKeyup,
126
157
  } = useList(props, emit);
127
158
  </script>
@@ -9,6 +9,8 @@ import type { ListPropTypes, ListEmitTypes, MenuListType, GroupedMenuListType }
9
9
  interface ListClasses {
10
10
  headerClasses: string;
11
11
  listItemClasses: string;
12
+ borderClasses: string;
13
+ listControlsClasses: string;
12
14
  }
13
15
 
14
16
  export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>['emit']) => {
@@ -23,13 +25,17 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
23
25
  disabledUnselectedItems,
24
26
  stickySearchOffset,
25
27
  allowDeselect,
28
+ allowSelectAll,
26
29
  } = toRefs(props);
27
30
 
28
31
  const listClasses: ComputedRef<ListClasses> = computed(() => {
32
+ const borderClasses = classNames('spr-border-color-weak spr-border spr-border-solid');
33
+
29
34
  const headerClasses = classNames(
30
35
  'spr-sticky spr-z-20',
31
36
  'spr-grid spr-gap-3 spr-bg-white-50 spr-px-size-spacing-3xs spr-py-size-spacing-2xs',
32
- 'spr-border-color-weak spr-border spr-border-x-0 spr-border-b spr-border-t-0 spr-border-solid',
37
+ borderClasses,
38
+ 'spr-border-x-0 spr-border-b spr-border-t-0',
33
39
  );
34
40
 
35
41
  const listItemClasses = classNames(
@@ -39,7 +45,9 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
39
45
  'active:spr-background-color-single-active active:spr-scale-[.98]',
40
46
  );
41
47
 
42
- return { headerClasses, listItemClasses };
48
+ const listControlsClasses = classNames('spr-flex spr-w-full spr-items-center');
49
+
50
+ return { headerClasses, listItemClasses, borderClasses, listControlsClasses };
43
51
  });
44
52
 
45
53
  const stickyOffsetStyle = computed(() => ({
@@ -640,6 +648,36 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
640
648
  event.preventDefault();
641
649
  };
642
650
 
651
+ // Computed property to check if any items are selected
652
+ const hasSelectedItems = computed(() => {
653
+ if (!multiSelect.value || !allowSelectAll.value) return false;
654
+ return selectedItems.value.length > 0;
655
+ });
656
+
657
+ // Function to handle select/unselect all items
658
+ const handleSelectAll = () => {
659
+ if (!multiSelect.value || !allowSelectAll.value) return;
660
+
661
+ const currentItems = hasGroupedItems.value
662
+ ? groupedMenuList.value.flatMap((group) => group.items)
663
+ : localizedMenuList.value;
664
+
665
+ // Filter out disabled items
666
+ const enabledItems = currentItems.filter((item) => !item.disabled);
667
+
668
+ if (hasSelectedItems.value) {
669
+ // If any items are selected, unselect all items completely
670
+ selectedItems.value = [];
671
+ // Also clear any preserved or API selected items
672
+ apiSelectedList.value = [];
673
+ emit('update:modelValue', []);
674
+ } else {
675
+ // If no items are selected, select all enabled items
676
+ selectedItems.value = [...enabledItems];
677
+ emit('update:modelValue', [...enabledItems]);
678
+ }
679
+ };
680
+
643
681
  return {
644
682
  listClasses,
645
683
  stickyOffsetStyle,
@@ -650,10 +688,12 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
650
688
  apiSelectedList,
651
689
  isParentMenu,
652
690
  hasGroupedItems,
691
+ hasSelectedItems,
653
692
  isItemSelected,
654
693
  getListItemClasses,
655
694
  handleSearch,
656
695
  handleSelectedItem,
696
+ handleSelectAll,
657
697
  trackNewlySelectedItems,
658
698
  handleSearchKeyup,
659
699
  };
@@ -209,6 +209,18 @@ export const multiSelectPropTypes = {
209
209
  type: Boolean,
210
210
  default: false,
211
211
  },
212
+ allowSelectAll: {
213
+ type: Boolean,
214
+ default: false,
215
+ },
216
+ avatarVariant: {
217
+ type: String,
218
+ default: '',
219
+ },
220
+ avatarSource: {
221
+ type: String,
222
+ default: '',
223
+ },
212
224
  };
213
225
 
214
226
  export const multiSelectEmitTypes = {
@@ -165,6 +165,9 @@
165
165
  :display-list-item-selected="props.displayListItemSelected"
166
166
  :disabled-local-search="props.disabledLocalSearch"
167
167
  :disabled-unselected-items="props.disabledUnselectedItems"
168
+ :allow-select-all="props.allowSelectAll"
169
+ :avatar-variant="props.avatarVariant"
170
+ :avatar-source="props.avatarSource"
168
171
  multi-select
169
172
  @update:model-value="handleMultiSelectedItem"
170
173
  @get-single-selected-item="emit('get-single-selected-item', $event)"
@@ -268,8 +268,9 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
268
268
  }
269
269
 
270
270
  // Always keep multiSelectedListItems in sync with selected values
271
+ const seenValues = new Set<string>();
271
272
  multiSelectedListItems.value = multiSelectOptions.value.filter((item) => {
272
- return values.some((val) => {
273
+ const isMatch = values.some((val) => {
273
274
  let itemVal = item.value;
274
275
  let valToCompare = val;
275
276
 
@@ -292,6 +293,16 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
292
293
  }
293
294
  return itemVal == valToCompare;
294
295
  });
296
+
297
+ if (isMatch) {
298
+ const itemKey = typeof item.value === 'object' ? JSON.stringify(item.value) : String(item.value);
299
+ if (seenValues.has(itemKey)) {
300
+ return false; // Skip duplicates
301
+ }
302
+ seenValues.add(itemKey);
303
+ }
304
+
305
+ return isMatch;
295
306
  });
296
307
 
297
308
  // Determine input text based on whether count-only mode is enabled