design-system-next 2.7.14 → 2.7.16

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,252 @@
1
+ <template>
2
+ <SprCard :has-content-padding="false">
3
+ <template #header>
4
+ <div :class="[getCalendarClasses.borderClasses, getCalendarClasses.headerWrapper]">
5
+ <div class="spr-flex spr-items-center spr-gap-size-spacing-3xs">
6
+ <div class="spr-flex">
7
+ <spr-button variant="tertiary" has-icon @click="prevWeek">
8
+ <Icon icon="ph:caret-left-fill" class="spr-text-color-success-base" />
9
+ </spr-button>
10
+ <spr-button variant="tertiary" has-icon @click="nextWeek">
11
+ <Icon icon="ph:caret-right-fill" class="spr-text-color-success-base" />
12
+ </spr-button>
13
+ </div>
14
+ <h2 class="spr-heading-xs">{{ weekRangeDisplay }}</h2>
15
+ </div>
16
+
17
+ <spr-button variant="secondary" size="small" @click="goToToday"> Today </spr-button>
18
+ </div>
19
+ </template>
20
+
21
+ <template #content>
22
+ <div :class="getCalendarClasses.contentWrapper">
23
+ <!-- Filters -->
24
+ <slot name="filter">
25
+ <div :class="getCalendarClasses.calendarFilter">
26
+ <spr-input v-model="searchTerm" placeholder="Search Employee">
27
+ <template #icon>
28
+ <Icon icon="ph:magnifying-glass" />
29
+ </template>
30
+ </spr-input>
31
+
32
+ <spr-dropdown
33
+ id="company-dropdown"
34
+ v-model="companyDropDown"
35
+ :menu-list="companyOptions"
36
+ @update:model-value="(e) => handleFilter('company', e[0])"
37
+ >
38
+ <!-- @update:model-value="(e) => (selectedCompany = e[0])" -->
39
+ <spr-input v-model="selectedCompany" placeholder="Company">
40
+ <template #icon>
41
+ <Icon icon="ph:caret-down" />
42
+ </template>
43
+ </spr-input>
44
+ </spr-dropdown>
45
+
46
+ <spr-dropdown
47
+ id="department-dropdown"
48
+ v-model="departmentDropDown"
49
+ :menu-list="departmentOptions"
50
+ @update:model-value="(e) => handleFilter('department', e[0])"
51
+ >
52
+ <spr-input v-model="selectedDepartment" placeholder="Department">
53
+ <template #icon>
54
+ <Icon icon="ph:caret-down" />
55
+ </template>
56
+ </spr-input>
57
+ </spr-dropdown>
58
+
59
+ <spr-dropdown
60
+ id="branch-dropdown"
61
+ v-model="branchDropDown"
62
+ :menu-list="branchOptions"
63
+ @update:model-value="(e) => handleFilter('branch', e[0])"
64
+ >
65
+ <spr-input v-model="selectedBranch" placeholder="Branch">
66
+ <template #icon>
67
+ <Icon icon="ph:caret-down" />
68
+ </template>
69
+ </spr-input>
70
+ </spr-dropdown>
71
+ </div>
72
+ </slot>
73
+
74
+ <div class="spr-table-wrapper">
75
+ <table :class="getCalendarClasses.calendarTable">
76
+ <!-- Calendar Header -->
77
+ <thead>
78
+ <tr>
79
+ <th :class="[getCalendarClasses.borderClasses, getCalendarClasses.tableHeaderEmployeeName]">
80
+ Employee Name
81
+ </th>
82
+ <th
83
+ v-for="(date, index) in weekDates"
84
+ :key="index"
85
+ :class="[getCalendarClasses.borderClasses, getCalendarClasses.tableHeader]"
86
+ >
87
+ <div :class="getCalendarClasses.headerContent">
88
+ <div
89
+ :class="[
90
+ getCalendarClasses.headerDate,
91
+ {
92
+ 'spr-background-color-brand-base spr-text-color-inverted-strong': isToday(date),
93
+ },
94
+ ]"
95
+ >
96
+ {{ formatDate(date, 'DD') }}
97
+ </div>
98
+ <div class="spr-body-sm-regular">
99
+ {{ formatDate(date, 'ddd').toUpperCase() }}
100
+ </div>
101
+ </div>
102
+ </th>
103
+ </tr>
104
+ </thead>
105
+ <tbody class="spr-h-[100vh] spr-overflow-y-auto">
106
+ <tr v-for="employee in employees" :key="employee.id">
107
+ <td
108
+ :class="[
109
+ getCalendarClasses.borderClasses,
110
+ 'spr-content-start spr-border-y spr-border-b-0 spr-border-l-0 spr-border-r spr-p-size-spacing-xs',
111
+ ]"
112
+ >
113
+ <div class="spr-flex spr-flex-col spr-gap-size-spacing-3xs">
114
+ <spr-avatar
115
+ :src="employee.avatar"
116
+ :initial="employee.name"
117
+ size="md"
118
+ :variant="employee.avatar ? 'image' : 'initial'"
119
+ color="tertiary"
120
+ />
121
+ <div class="spr-label-xs-regular">{{ employee.name }}</div>
122
+ <div class="spr-text-color-supporting spr-label-xs-regular spr-uppercase">
123
+ {{ employee.position }}
124
+ </div>
125
+ </div>
126
+ <div class="spr-mt-size-spacing-xs">
127
+ <spr-lozenge
128
+ :label="`${employee.hoursWorked || 0}/${employee.hoursTarget || 48} HRS`"
129
+ tone="neutral"
130
+ >
131
+ <template #icon>
132
+ <Icon icon="ph:clock" />
133
+ </template>
134
+ </spr-lozenge>
135
+ </div>
136
+ </td>
137
+ <td
138
+ v-for="(date, index) in weekDates"
139
+ :key="index"
140
+ :class="[
141
+ getCalendarClasses.borderClasses,
142
+ 'spr-content-start spr-space-y-size-spacing-3xs spr-border-x spr-border-b-0 spr-border-t spr-p-size-spacing-sm last:spr-border-r-0',
143
+ ]"
144
+ @mouseover="handleHover(true, index, employee.id)"
145
+ @mouseleave="handleHover(false, index, employee.id)"
146
+ >
147
+ <section
148
+ v-if="
149
+ employee.schedule[formatDate(date, dateFormat)] &&
150
+ employee.schedule[formatDate(date, dateFormat)].length > 0
151
+ "
152
+ class="spr-flex spr-flex-col spr-justify-start spr-gap-size-spacing-3xs"
153
+ >
154
+ <div
155
+ v-for="(schedule, scheduleIndex) in employee.schedule[formatDate(date, dateFormat)]"
156
+ :key="scheduleIndex"
157
+ >
158
+ <div
159
+ v-if="schedule.type === 'restday'"
160
+ @click="
161
+ onShiftClick({
162
+ employeeId: employee.id,
163
+ date: formatDate(date, dateFormat),
164
+ shift: 'restday',
165
+ })
166
+ "
167
+ >
168
+ <spr-calendar-cell type="restday" />
169
+ </div>
170
+ <div
171
+ v-else
172
+ @click="
173
+ onShiftClick({
174
+ employeeId: employee.id,
175
+ date: formatDate(date, dateFormat),
176
+ shift: schedule,
177
+ })
178
+ "
179
+ >
180
+ <spr-calendar-cell
181
+ :view-only="false"
182
+ :title="`${schedule.startTime} - ${schedule.endTime}`"
183
+ :description="schedule.location"
184
+ :sub-description="schedule.type"
185
+ />
186
+ </div>
187
+ </div>
188
+ </section>
189
+
190
+ <section v-if="showAddShift(index, employee.id)">
191
+ <spr-calendar-cell
192
+ status="pending"
193
+ type="exempt"
194
+ :view-only="false"
195
+ @on-click="
196
+ onShiftClick({ employeeId: employee.id, date: formatDate(date, dateFormat), shift: null })
197
+ "
198
+ >
199
+ <Icon icon="ph:plus" />
200
+ <div class="spr-label-xs-medium">Add New Shift</div>
201
+ </spr-calendar-cell>
202
+ </section>
203
+ </td>
204
+ </tr>
205
+ </tbody>
206
+ </table>
207
+ </div>
208
+ </div>
209
+ </template>
210
+ </SprCard>
211
+ </template>
212
+
213
+ <script setup lang="ts">
214
+ import { Icon } from '@iconify/vue';
215
+ import SprButton from '@/components/button/button.vue';
216
+ import SprInput from '@/components/input/input.vue';
217
+ import SprAvatar from '@/components/avatar/avatar.vue';
218
+ import SprCard from '@/components/card/card.vue';
219
+ import SprDropdown from '@/components/dropdown/dropdown.vue';
220
+ import SprLozenge from '@/components/lozenge/lozenge.vue';
221
+ import SprCalendarCell from '@/components/calendar-cell/calendar-cell.vue';
222
+ import { calendarPropTypes, calendarEmitTypes } from './calendar';
223
+
224
+ const props = defineProps(calendarPropTypes);
225
+ import { useCalendar } from './use-calendar';
226
+ const emit = defineEmits(calendarEmitTypes);
227
+
228
+ const {
229
+ // State
230
+ searchTerm,
231
+ selectedCompany,
232
+ selectedDepartment,
233
+ selectedBranch,
234
+ weekDates,
235
+ weekRangeDisplay,
236
+ getCalendarClasses,
237
+ companyDropDown,
238
+ departmentDropDown,
239
+ branchDropDown,
240
+ dateFormat,
241
+ // Function
242
+ formatDate,
243
+ isToday,
244
+ prevWeek,
245
+ nextWeek,
246
+ goToToday,
247
+ onShiftClick,
248
+ handleHover,
249
+ showAddShift,
250
+ handleFilter,
251
+ } = useCalendar(props, emit);
252
+ </script>
@@ -0,0 +1,159 @@
1
+ import { computed, SetupContext, toRefs, ref, watch } from 'vue';
2
+ import dayjs from 'dayjs';
3
+ import classNames from 'classnames';
4
+ import { useVModel } from '@vueuse/core';
5
+
6
+ import type { CalendarPropTypes, CalendarEmitTypes, SelectedShift } from './calendar';
7
+
8
+ export const useCalendar = (props: CalendarPropTypes, emit: SetupContext<CalendarEmitTypes>['emit']) => {
9
+ const { initialDate, companyOptions, departmentOptions, branchOptions } = toRefs(props);
10
+
11
+ const state = {
12
+ dateFormat: ref('YYYY-MM-DD'),
13
+ companyDropDown: ref([]),
14
+ departmentDropDown: ref([]),
15
+ branchDropDown: ref([]),
16
+ currentDate: ref(dayjs(initialDate.value)),
17
+ searchTerm: ref<string>(''),
18
+ selectedCompany: ref<string>(''),
19
+ selectedDepartment: ref<string>(''),
20
+ selectedBranch: ref<string>(''),
21
+ isHover: ref<boolean>(false),
22
+ hoveredCell: ref<number>(),
23
+ employeeId: ref<number>(),
24
+ };
25
+
26
+ const searchEmployee = useVModel(props, 'search', emit);
27
+ const selectedCell = useVModel(props, 'selectedCell', emit);
28
+ const selectedCompany = useVModel(props, 'selectedCompany', emit);
29
+ const selectedDepartment = useVModel(props, 'selectedDepartment', emit);
30
+ const selectedBranch = useVModel(props, 'selectedBranch', emit);
31
+
32
+ const startDate = computed(() => state.currentDate.value.startOf('week'));
33
+
34
+ const weekDates = computed(() => {
35
+ return Array.from({ length: 7 }).map((_, index) => startDate.value.add(index, 'day'));
36
+ });
37
+
38
+ const weekRangeDisplay = computed(() => {
39
+ const weekStart = startDate.value;
40
+ const weekEnd = startDate.value.add(6, 'day');
41
+ const format = 'MMM YYYY';
42
+
43
+ if (weekStart.format('YYYY') !== weekEnd.format('YYYY')) {
44
+ return `${weekStart.format(format)} - ${weekEnd.format(format)}`;
45
+ }
46
+
47
+ if (weekStart.format('MMM') !== weekEnd.format('MMM')) {
48
+ return `${weekStart.format('MMM')} - ${weekEnd.format(format)}`;
49
+ }
50
+
51
+ return `${weekStart.format(format)}`;
52
+ });
53
+
54
+ const formatDate = (date: dayjs.Dayjs, formatStr: string) => date.format(formatStr);
55
+
56
+ const isToday = (date: dayjs.Dayjs) => date.isSame(dayjs(), 'day');
57
+
58
+ const prevWeek = () => {
59
+ state.currentDate.value = state.currentDate.value.subtract(1, 'week');
60
+ };
61
+
62
+ const nextWeek = () => {
63
+ state.currentDate.value = state.currentDate.value.add(1, 'week');
64
+ };
65
+
66
+ const goToToday = () => {
67
+ state.currentDate.value = dayjs();
68
+ };
69
+
70
+ const onShiftClick = (selected: SelectedShift) => {
71
+ selectedCell.value = selected;
72
+ };
73
+
74
+ const handleHover = (isHover: boolean, index: number, employeeId: number) => {
75
+ state.isHover.value = isHover;
76
+ state.hoveredCell.value = isHover ? index : null;
77
+ state.employeeId.value = employeeId;
78
+ };
79
+
80
+ const showAddShift = (index: number, employeeId: number) => {
81
+ return state.hoveredCell.value === index && state.isHover.value && state.employeeId.value === employeeId;
82
+ };
83
+
84
+ const handleFilter = (filter: string, selected: string) => {
85
+ if (filter === 'company') {
86
+ const foundCompany = companyOptions.value.find((item) => item.value === selected);
87
+ state.selectedCompany.value = foundCompany?.text ?? '';
88
+ selectedCompany.value = selected;
89
+ }
90
+
91
+ if (filter === 'department') {
92
+ const foundCompany = departmentOptions.value.find((item) => item.value === selected);
93
+ state.selectedDepartment.value = foundCompany?.text ?? '';
94
+ selectedDepartment.value = selected;
95
+ }
96
+
97
+ if (filter === 'branch') {
98
+ const foundCompany = branchOptions.value.find((item) => item.value === selected);
99
+ state.selectedBranch.value = foundCompany?.text ?? '';
100
+ selectedBranch.value = selected;
101
+ }
102
+ };
103
+
104
+ watch(state.searchTerm, (value, oldValue) => {
105
+ if (value === oldValue) return; // Prevent unnecessary updates
106
+ searchEmployee.value = value;
107
+ });
108
+
109
+ const getCalendarClasses = computed(() => {
110
+ const borderClasses = classNames(' spr-border spr-border-color-weak spr-border-solid');
111
+ const headerWrapper = classNames(
112
+ 'spr-bg-color-weak spr-flex spr-w-full spr-items-center spr-justify-between spr-border-x-0 spr-border-b spr-border-t-0 spr-p-size-spacing-sm',
113
+ );
114
+
115
+ const contentWrapper = classNames('spr-divide-color-weak spr-divide-x-0 spr-divide-y spr-divide-solid');
116
+ const calendarFilter = classNames('spr-grid spr-grid-cols-4 spr-gap-size-spacing-2xs spr-p-size-spacing-xs');
117
+ const calendarTable = classNames(
118
+ 'spr-table spr-w-full spr-table-fixed spr-border-collapse spr-border-spacing-0 spr-overflow-hidden spr-rounded-border',
119
+ );
120
+ const tableHeaderEmployeeName = classNames(
121
+ 'spr-body-xs-regular-medium spr-border-x spr-border-y spr-p-size-spacing-xs spr-text-left first:spr-border-l-0',
122
+ );
123
+ const tableHeader = classNames('spr-border-x-0 spr-border-y spr-border-l spr-p-size-spacing-sm spr-text-center');
124
+ const headerContent = classNames('spr-flex spr-w-full spr-items-center spr-gap-size-spacing-3xs');
125
+ const headerDate = classNames(
126
+ 'spr-font-size-400 spr-line-height-500 spr-letter-spacing-dense spr-flex spr-h-size-spacing-md spr-w-size-spacing-md spr-items-center spr-justify-center spr-rounded-full spr-font-main spr-font-normal',
127
+ );
128
+
129
+ return {
130
+ borderClasses,
131
+ headerWrapper,
132
+ contentWrapper,
133
+ calendarFilter,
134
+ calendarTable,
135
+ tableHeaderEmployeeName,
136
+ tableHeader,
137
+ headerContent,
138
+ headerDate,
139
+ };
140
+ });
141
+
142
+ return {
143
+ weekDates,
144
+ weekRangeDisplay,
145
+ getCalendarClasses,
146
+
147
+ formatDate,
148
+ isToday,
149
+ prevWeek,
150
+ nextWeek,
151
+ goToToday,
152
+ onShiftClick,
153
+ handleHover,
154
+ showAddShift,
155
+ handleFilter,
156
+
157
+ ...state,
158
+ };
159
+ };
@@ -37,6 +37,10 @@ export const calendarCellPropTypes = {
37
37
  type: Boolean,
38
38
  default: true,
39
39
  },
40
+ subDescription: {
41
+ type: String,
42
+ default: '',
43
+ },
40
44
  };
41
45
 
42
46
  export type CalendarCellPropTypes = ExtractPropTypes<typeof calendarCellPropTypes>;
@@ -53,7 +53,7 @@ export const useCalendarCell = (props: CalendarCellPropTypes, emit: SetupContext
53
53
  const isError = computed(() => props.status === 'error');
54
54
 
55
55
  const getShiftLabel = computed((): string => {
56
- return shiftLabels[props.type] || props.type;
56
+ return shiftLabels[props.type] || props.subDescription;
57
57
  });
58
58
 
59
59
  const getCellIcon = computed((): string => {
@@ -38,6 +38,10 @@ export const cardPropTypes = {
38
38
  type: Boolean,
39
39
  default: false,
40
40
  },
41
+ hasContentPadding: {
42
+ type: Boolean,
43
+ default: true,
44
+ },
41
45
  };
42
46
 
43
47
  export type CardPropTypes = ExtractPropTypes<typeof cardPropTypes>;
@@ -11,7 +11,7 @@ interface CardClasses {
11
11
  }
12
12
 
13
13
  export const useCard = (props: CardPropTypes, slots: Slots) => {
14
- const { title, headerIcon, borderRadiusSize, hasCollapsible, isCollapsibleOpen } = toRefs(props);
14
+ const { title, headerIcon, borderRadiusSize, hasCollapsible, isCollapsibleOpen, hasContentPadding } = toRefs(props);
15
15
 
16
16
  const cardClasses: ComputedRef<CardClasses> = computed(() => {
17
17
  const baseClasses = classNames(
@@ -40,7 +40,9 @@ export const useCard = (props: CardPropTypes, slots: Slots) => {
40
40
  'spr-rounded-t-border-radius-xl': borderRadiusSize.value === 'xl' || !borderRadiusSize.value,
41
41
  });
42
42
 
43
- const contentPaddingSizeClasses = classNames('spr-py-size-spacing-2xs spr-px-size-spacing-xs');
43
+ const contentPaddingSizeClasses = classNames({
44
+ 'spr-py-size-spacing-2xs spr-px-size-spacing-xs': hasContentPadding.value,
45
+ });
44
46
 
45
47
  const footerClasses = classNames(
46
48
  `spr-flex spr-items-center spr-border-0 spr-border-t spr-border-solid spr-border-mushroom-200 spr-py-size-spacing-2xs spr-px-size-spacing-xs`,
@@ -8,10 +8,12 @@ import { useInfiniteScroll, onClickOutside } from '@vueuse/core';
8
8
 
9
9
  export const useFilter = (props: FilterPropTypes, emit: SetupContext<FilterEmitTypes>['emit']) => {
10
10
  const { options, filterMenu, filterData, loading, filterable, filling, deselected, hasSearchApi } = toRefs(props);
11
+
11
12
  const selectedValue = useVModel(props, 'modelValue', emit);
12
- const isFilterOpen = ref<boolean>(false);
13
13
  const searchText = useVModel(props, 'search', emit);
14
14
  const searchFilterValue = useVModel(props, 'searchFilter', emit);
15
+
16
+ const isFilterOpen = ref<boolean>(false);
15
17
  const searchValue = ref<string>('');
16
18
  const filterMenuSearchvalue = ref<string>('');
17
19
  const isAddFilterVisible = ref<boolean>(false);
@@ -27,6 +27,13 @@ export const switchPropTypes = {
27
27
  required: true,
28
28
  default: false,
29
29
  },
30
+ /**
31
+ * @description Switch id
32
+ */
33
+ id: {
34
+ type: String,
35
+ default: '',
36
+ },
30
37
  };
31
38
 
32
39
  export const switchEmitTypes = ['update:modelValue'];
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div v-bind="switchProps" :class="['spr-flex spr-items-center spr-gap-2', switchTextClass]">
3
- <label v-if="!isSlotEmpty">
3
+ <label v-if="!isLeftTextLabel" :for="defaultId" class="spr-cursor-pointer">
4
4
  <slot name="leftText">
5
5
  <slot></slot>
6
6
  </slot>
@@ -16,6 +16,7 @@
16
16
  ref="switchRef"
17
17
  v-model="proxyValue"
18
18
  type="checkbox"
19
+ :id="defaultId"
19
20
  name="checkbox"
20
21
  :class="[
21
22
  'input spr-absolute spr-left-0 spr-top-0 spr-z-10 spr-m-0 spr-h-6 spr-w-12 spr-opacity-0',
@@ -30,7 +31,7 @@
30
31
  ]"
31
32
  ></span>
32
33
  </div>
33
- <label v-if="!isSlotEmpty">
34
+ <label v-if="!isRightTextLabel" :for="defaultId" class="spr-cursor-pointer">
34
35
  <slot name="rightText"></slot>
35
36
  </label>
36
37
  </div>
@@ -45,9 +46,11 @@ import { useSwitch } from './use-switch';
45
46
  const props = defineProps(switchPropTypes);
46
47
  const emit = defineEmits(switchEmitTypes);
47
48
 
49
+ const defaultId = props.id ? props.id + '_' + Math.random().toString(36).substring(2, 8) : 'switch_input_' + Math.random().toString(36).substring(2, 8);
50
+
48
51
  const proxyValue = useVModel(props, 'modelValue', emit);
49
52
 
50
- const { switchWrapperRef, switchRef, switchProps, switchMarkClass, switchTextClass, switchInputClass, isSlotEmpty } =
53
+ const { switchWrapperRef, switchRef, switchProps, switchMarkClass, switchTextClass, switchInputClass, isLeftTextLabel, isRightTextLabel } =
51
54
  useSwitch(props);
52
55
  </script>
53
56
 
@@ -15,7 +15,8 @@ export const useSwitch = (props: SwitchPropTypes) => {
15
15
  const slots = useSlots();
16
16
 
17
17
  // if the slot label is empty, we will not show the label
18
- const isSlotEmpty = (!slots.default || slots.default().length === 0) && !slots.leftText && !slots.rightText;
18
+ const isLeftTextLabel = (!slots.default || slots.default().length === 0) && !slots.leftText;
19
+ const isRightTextLabel = !slots.rightText;
19
20
 
20
21
  const switchProps: ComputedRef<Record<string, unknown>> = computed(() => {
21
22
  return {
@@ -100,6 +101,7 @@ export const useSwitch = (props: SwitchPropTypes) => {
100
101
  switchMarkClass,
101
102
  switchTextClass,
102
103
  switchInputClass,
103
- isSlotEmpty
104
+ isLeftTextLabel,
105
+ isRightTextLabel
104
106
  };
105
107
  };
package/src/vite-env.d.ts DELETED
@@ -1,6 +0,0 @@
1
- interface ImportMeta { readonly glob: ImportMetaGlob }
2
-
3
- interface ImportMetaGlob {
4
- (pattern: string, options?: { eager?: boolean }): Record<string, () => Promise<unknown>>
5
- (pattern: string, options?: { eager: true }): Record<string, unknown>
6
- }