sprintify-ui 0.0.141 → 0.0.142

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.
@@ -6,13 +6,22 @@ import { createFieldStory } from '../../.storybook/utils';
6
6
  export default {
7
7
  title: 'Form/BaseDatePicker',
8
8
  component: BaseDatePicker,
9
- argTypes: {},
9
+ argTypes: {
10
+ mode: {
11
+ control: { type: 'select' },
12
+ options: ['single', 'multiple', 'range', 'time'],
13
+ },
14
+ locale: {
15
+ control: { type: 'select' },
16
+ options: ['en', 'fr'],
17
+ },
18
+ },
10
19
  };
11
20
 
12
21
  const Template = (args) => ({
13
22
  components: { BaseDatePicker, ShowValue },
14
23
  setup() {
15
- const value = ref('2022-11-16T10:00:00Z');
24
+ const value = ref('2023-01-10T10:00:00Z');
16
25
  return { value, args };
17
26
  },
18
27
  template: `
@@ -24,19 +33,14 @@ const Template = (args) => ({
24
33
 
25
34
  export const Demo = Template.bind({});
26
35
 
27
- export const YearRange = Template.bind({});
28
- YearRange.args = {
29
- yearRange: [1920, 2020],
30
- };
31
-
32
36
  export const MinDate = Template.bind({});
33
37
  MinDate.args = {
34
- minDate: DateTime.fromISO('2022-11-10').toJSDate(),
38
+ minDate: DateTime.fromISO('2023-01-05').toJSDate(),
35
39
  };
36
40
 
37
41
  export const MaxDate = Template.bind({});
38
42
  MaxDate.args = {
39
- maxDate: DateTime.fromISO('2022-11-20').toJSDate(),
43
+ maxDate: '2023-01-18',
40
44
  };
41
45
 
42
46
  export const Disabled = Template.bind({});
@@ -44,6 +48,46 @@ Disabled.args = {
44
48
  disabled: true,
45
49
  };
46
50
 
51
+ export const Range = Template.bind({});
52
+ Range.args = {
53
+ mode: 'range',
54
+ };
55
+
56
+ export const Multiple = Template.bind({});
57
+ Multiple.args = {
58
+ mode: 'multiple',
59
+ };
60
+
61
+ export const DateAndTime = Template.bind({});
62
+ DateAndTime.args = {
63
+ enableTime: true,
64
+ format: 'Y-m-d H:i',
65
+ };
66
+
67
+ export const DisableDates = Template.bind({});
68
+ DisableDates.args = {
69
+ disableDates: [
70
+ '2023-01-12',
71
+ '2023-01-18',
72
+ '2023-01-24',
73
+ new Date(2023, 0, 16), // '2023-01-16'
74
+ ],
75
+ };
76
+
77
+ export const RangeDisableDates = Template.bind({});
78
+ RangeDisableDates.args = {
79
+ disableDates: [
80
+ {
81
+ from: '2023-01-09',
82
+ to: '2023-01-15',
83
+ },
84
+ {
85
+ from: '2023-01-20',
86
+ to: '2023-01-25',
87
+ },
88
+ ],
89
+ };
90
+
47
91
  export const Inline = Template.bind({});
48
92
  Inline.args = {
49
93
  inline: true,
@@ -71,7 +115,7 @@ export const DateTimeConversion = (args) => {
71
115
  };
72
116
  };
73
117
  DateTimeConversion.args = {
74
- modelValue: '2022-11-05T10:00:00Z',
118
+ modelValue: '2023-01-05T10:00:00Z',
75
119
  };
76
120
 
77
121
  export const Field = createFieldStory({
@@ -13,17 +13,16 @@
13
13
  </div>
14
14
  <input
15
15
  ref="datepicker"
16
- :value="modelValueFormatted"
17
16
  type="text"
18
17
  readonly
19
18
  :disabled="disabled"
20
- class="h-10 w-full rounded pl-10 pr-16 disabled:cursor-not-allowed disabled:text-slate-300"
19
+ class="flatpickr w-full rounded pl-10 pr-16 disabled:cursor-not-allowed disabled:text-slate-300"
21
20
  :class="[hasErrorInternal ? 'border-red-500' : 'border-slate-300']"
22
21
  :placeholder="$t('sui.click_or_select_date')"
23
22
  />
24
23
  <div
25
24
  v-if="modelValueFormatted && !disabled"
26
- class="absolute top-0 right-0 flex h-10 items-center justify-center p-1"
25
+ class="absolute right-0 top-0.5 flex h-full justify-center p-1"
27
26
  >
28
27
  <button
29
28
  type="button"
@@ -37,17 +36,24 @@
37
36
  </template>
38
37
 
39
38
  <script lang="ts" setup>
40
- import { PropType } from 'vue';
41
- import Pikaday from 'pikaday';
42
- import { capitalize, padStart } from 'lodash';
43
- import { DateTime, Info } from 'luxon';
39
+ import { PropType, Ref } from 'vue';
40
+ import { isArray } from 'lodash';
41
+ import { DateTime } from 'luxon';
44
42
  import { BaseIcon } from '.';
45
43
  import { useField } from '@/composables/field';
44
+ import flatpickr from 'flatpickr';
45
+ import 'flatpickr/dist/flatpickr.css';
46
+
47
+ import { French } from 'flatpickr/dist/l10n/fr';
48
+ import { english } from 'flatpickr/dist/l10n/default';
49
+ import { Instance } from 'flatpickr/dist/types/instance';
46
50
 
47
51
  const props = defineProps({
48
52
  modelValue: {
49
53
  default: undefined,
50
- type: [String, null] as PropType<string | null | undefined>,
54
+ type: [String, null, Array] as PropType<
55
+ string | null | string[] | undefined
56
+ >,
51
57
  },
52
58
  required: {
53
59
  default: false,
@@ -58,16 +64,12 @@ const props = defineProps({
58
64
  type: Boolean,
59
65
  },
60
66
  minDate: {
61
- default: undefined,
62
- type: Date,
67
+ default: null,
68
+ type: [String, Date],
63
69
  },
64
70
  maxDate: {
65
- default: undefined,
66
- type: Date,
67
- },
68
- yearRange: {
69
- default: undefined,
70
- type: [Number, Array] as PropType<number | [number, number]>,
71
+ default: null,
72
+ type: [String, Date],
71
73
  },
72
74
  hasError: {
73
75
  default: false,
@@ -81,6 +83,34 @@ const props = defineProps({
81
83
  default: false,
82
84
  type: Boolean,
83
85
  },
86
+ enableTime: {
87
+ default: false,
88
+ type: Boolean,
89
+ },
90
+ format: {
91
+ default: 'Y-m-d',
92
+ type: String,
93
+ },
94
+ mode: {
95
+ default: 'single',
96
+ type: String as PropType<
97
+ 'single' | 'multiple' | 'range' | 'time' | undefined
98
+ >,
99
+ },
100
+ locale: {
101
+ default: 'en',
102
+ type: String as PropType<'en' | 'fr' | undefined>,
103
+ },
104
+ noCalendar: {
105
+ default: false,
106
+ type: Boolean,
107
+ },
108
+ disableDates: {
109
+ type: [Array, Object] as PropType<string[] | Date[]>,
110
+ default() {
111
+ return [];
112
+ },
113
+ },
84
114
  });
85
115
 
86
116
  const emit = defineEmits(['update:modelValue']);
@@ -92,35 +122,52 @@ const { hasErrorInternal, emitUpdate } = useField({
92
122
  emit: emit,
93
123
  });
94
124
 
95
- const i18n = useI18n();
125
+ const modelValueFormatted = computed((): string | string[] | null => {
126
+ if (!props.modelValue) {
127
+ return null;
128
+ }
96
129
 
97
- const datepicker = ref<HTMLInputElement | null>(null);
130
+ if (isArray(props.modelValue)) {
131
+ const listModelValue = [] as string[];
98
132
 
99
- const months = Info.months('long', { locale: i18n.locale.value }).map((m) =>
100
- capitalize(m)
101
- );
102
- const weekdays = arrayRotate(
103
- Info.weekdays('long', { locale: i18n.locale.value }).map((m) =>
104
- capitalize(m)
105
- ),
106
- true
107
- );
108
- const weekdaysShort = arrayRotate(
109
- Info.weekdays('short', { locale: i18n.locale.value }).map((m) =>
110
- capitalize(m)
111
- ),
112
- true
113
- );
133
+ props.modelValue.forEach(function (date) {
134
+ listModelValue.push(DateTime.fromISO(date, { zone: 'utc' }).toISODate());
135
+ });
114
136
 
115
- const modelValueFormatted = computed((): string | null => {
116
- if (!props.modelValue) {
117
- return null;
137
+ return listModelValue;
118
138
  }
139
+
119
140
  return (
120
141
  DateTime.fromISO(props.modelValue, { zone: 'utc' }).toISODate() ?? null
121
142
  );
122
143
  });
123
144
 
145
+ const locale = computed(() => {
146
+ if (props.locale === 'fr') {
147
+ return French;
148
+ }
149
+
150
+ return english;
151
+ });
152
+
153
+ const datepicker = ref(null) as Ref<HTMLInputElement | null>;
154
+
155
+ let picker = null as Instance | null;
156
+
157
+ const flatpickrConfig = computed(() => {
158
+ return {
159
+ enableTime: props.enableTime,
160
+ dateFormat: props.format,
161
+ mode: props.mode,
162
+ locale: locale.value,
163
+ minDate: props.minDate,
164
+ maxDate: props.maxDate,
165
+ noCalendar: props.noCalendar,
166
+ disable: props.disableDates,
167
+ inline: props.inline,
168
+ };
169
+ });
170
+
124
171
  // Make sure the model value is always formatted on the parent component
125
172
  watch(
126
173
  () => modelValueFormatted.value,
@@ -132,230 +179,122 @@ watch(
132
179
  { immediate: true }
133
180
  );
134
181
 
135
- let picker = null as Pikaday | null;
182
+ /*
183
+ | Flatpickr does not seem to observe changes in the value attribute of the input.
184
+ | We should watch on modelValue,
185
+ | and manually react and apply the changes on the picker
186
+ */
187
+ watch(
188
+ () => props.modelValue,
189
+ (newValue) => {
190
+ if (!picker) {
191
+ return;
192
+ }
193
+
194
+ if (!newValue) {
195
+ picker.clear();
196
+ return;
197
+ }
198
+
199
+ picker.setDate(newValue, false);
200
+ }
201
+ );
202
+
203
+ /*
204
+ | If the props in flatpickrConfig are changed
205
+ | update the flatpickr instance
206
+ */
207
+ watch(
208
+ () => flatpickrConfig,
209
+ () => init(),
210
+ {
211
+ deep: true,
212
+ immediate: true,
213
+ }
214
+ );
136
215
 
137
216
  onMounted(() => {
217
+ init();
218
+ });
219
+
220
+ onBeforeUnmount(() => {
221
+ if (picker) {
222
+ picker.destroy();
223
+ }
224
+ });
225
+
226
+ function init() {
138
227
  if (!datepicker.value) {
139
228
  return;
140
229
  }
141
230
 
142
- picker = new Pikaday({
143
- theme: 'pikaday-white',
144
- field: datepicker.value,
145
- yearRange: props.yearRange,
146
- bound: !props.inline,
147
- i18n: {
148
- previousMonth: i18n.t('sui.previous_month'),
149
- nextMonth: i18n.t('sui.next_month'),
150
- months: months,
151
- weekdays: weekdays,
152
- weekdaysShort: weekdaysShort,
153
- },
154
- toString(date) {
155
- const day = padStart(date.getDate() + '', 2, '0');
156
- const month = padStart(date.getMonth() + 1 + '', 2, '0');
157
- const year = padStart(date.getFullYear() + '', 4, '0');
158
-
159
- return `${year}-${month}-${day}`;
160
- },
161
- parse(dateString) {
162
- if (!dateString) {
163
- return null;
231
+ picker = flatpickr(datepicker.value, {
232
+ ...flatpickrConfig.value,
233
+ onChange(dates) {
234
+ if (!dates.length) {
235
+ clear();
236
+ return;
164
237
  }
165
238
 
166
- const datetime = DateTime.fromISO(dateString, { zone: 'utc' });
167
- const year = datetime.year;
168
- const month = datetime.month - 1;
169
- const day = datetime.day;
239
+ const datetime = DateTime.fromJSDate(dates[0]);
170
240
 
171
- return new Date(year, month, day);
172
- },
173
- minDate: props.minDate,
174
- maxDate: props.maxDate,
175
- onSelect(date) {
176
- if (!date) {
241
+ if (!datetime) {
177
242
  clear();
178
243
  return;
179
244
  }
180
245
 
181
- const datetime = DateTime.fromJSDate(date);
246
+ const listDate = [] as string[];
182
247
 
183
- if (!datetime) {
184
- clear();
248
+ dates.forEach(function (date) {
249
+ if (props.enableTime) {
250
+ listDate.push(DateTime.fromJSDate(date, { zone: 'utc' }).toISO());
251
+ } else {
252
+ listDate.push(DateTime.fromJSDate(date, { zone: 'utc' }).toISODate());
253
+ }
254
+ });
255
+
256
+ if (listDate.length == 0) {
257
+ emitUpdate(null);
185
258
  return;
186
259
  }
187
260
 
188
- emitUpdate(datetime.toISODate());
261
+ if (props.mode === 'range' || props.mode === 'multiple') {
262
+ emitUpdate(listDate);
263
+ return;
264
+ }
265
+
266
+ emitUpdate(listDate[0]);
189
267
  },
190
268
  });
191
- });
192
-
193
- onBeforeUnmount(() => {
194
- if (picker) {
195
- picker.destroy();
196
- }
197
- });
269
+ }
198
270
 
199
271
  function clear() {
200
- picker?.clear();
201
272
  emitUpdate(null);
202
273
  }
203
-
204
- // https://stackoverflow.com/a/23368052
205
- function arrayRotate(arr: any[], reverse = false): any[] {
206
- if (reverse) arr.unshift(arr.pop());
207
- else arr.push(arr.shift());
208
- return arr;
209
- }
210
274
  </script>
211
-
212
275
  <style lang="postcss">
213
- @import 'pikaday/css/pikaday.css';
214
-
215
276
  .base-date-picker--inline {
216
- .pikaday-white.pika-single {
277
+ .flatpickr-calendar.inline {
217
278
  box-shadow: none;
218
279
  border: none;
219
280
  padding: 0;
281
+ margin-top: 6px;
220
282
  }
221
- .pika-lendar {
222
- margin: 0;
223
- margin-top: 0.25rem;
224
- }
225
- }
226
-
227
- .pikaday-white {
228
- --backgroundColor: #ffffff;
229
- --textColor: #718096;
230
- --currentDateTextColor: #3182ce;
231
- --selectedDateBackgroundColor: #3182ce;
232
- --selectedDateTextColor: #ffffff;
233
-
234
- --labelTextColor: #4a5568; /* eg. May 2020 */
235
- --weekDaysTextColor: #a0aec0; /* eg. Mo Tu We ....*/
236
-
237
- font-family: inherit;
238
- background-color: var(--backgroundColor);
239
- padding: 0.5rem;
240
- z-index: 60;
241
- margin: 6px 0 0 0;
242
-
243
- @apply shadow;
244
- @apply rounded-lg;
245
- }
246
-
247
- .pikaday-white .pika-title {
248
- width: 100%;
249
- text-align: center;
250
- display: flex;
251
- justify-content: flex-start;
252
- margin-bottom: 5px;
253
- }
254
-
255
- /* Next/Previous */
256
- .pikaday-white .pika-prev,
257
- .pikaday-white .pika-next {
258
- position: absolute;
259
- outline: none;
260
- padding: 0;
261
- width: 28px;
262
- height: 28px;
263
- top: -1px;
264
- display: inline-block;
265
- margin-top: 0;
266
- cursor: pointer;
267
- /* hide text using text-indent trick, using width value (it's enough) */
268
- text-indent: -9999px;
269
- white-space: nowrap;
270
- overflow: hidden;
271
- background-color: transparent;
272
- background-position: center center;
273
- background-repeat: no-repeat;
274
- opacity: 0.8;
275
- }
276
- .pikaday-white .pika-prev:hover,
277
- .pikaday-white .pika-next:hover {
278
- opacity: 1;
279
283
  }
280
- .pikaday-white .pika-prev {
281
- right: 30px;
282
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15 19l-7-7 7-7'%3E%3C/path%3E%3C/svg%3E");
284
+ .flatpickr-calendar .flatpickr-current-month .flatpickr-monthDropdown-months {
285
+ font-weight: 400;
283
286
  }
284
- .pikaday-white .pika-next {
285
- right: 2px;
286
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 5l7 7-7 7'%3E%3C/path%3E%3C/svg%3E");
287
+ .flatpickr-calendar .flatpickr-current-month input.cur-year {
288
+ font-weight: 400;
287
289
  }
288
- .pika-prev.is-disabled,
289
- .pika-next.is-disabled {
290
- cursor: default;
291
- opacity: 0.2;
290
+ .flatpickr-months .flatpickr-prev-month:hover svg,
291
+ .flatpickr-months .flatpickr-next-month:hover svg {
292
+ fill: #888;
292
293
  }
293
-
294
- .pikaday-white .pika-label {
295
- @apply mr-1.5 rounded border border-slate-200 py-1 px-2 text-[15px] font-normal leading-none;
296
- }
297
-
298
- /* Show Month & Year select */
299
- .pikaday-white .pika-label {
300
- position: relative; /* add this */
301
- /* some additional code */
294
+ .flatpickr-calendar span.flatpickr-weekday {
295
+ @apply font-medium text-slate-400;
302
296
  }
303
-
304
- .pikaday-white .pika-select-month,
305
- .pikaday-white .pika-select-year {
306
- /*display: none;*/
307
- width: 100%;
308
- cursor: pointer;
309
- position: absolute;
310
- z-index: 9998;
311
- margin: 0;
312
- left: 0;
313
- top: 0;
314
- opacity: 0;
315
- padding: 0px;
316
- }
317
-
318
- .pikaday-white table {
319
- width: 100%;
320
- border-collapse: collapse;
321
- }
322
-
323
- .pikaday-white table th {
324
- width: 2.5em;
325
- height: 2.5em;
326
- font-weight: normal;
327
- color: var(--weekDaysTextColor);
328
- font-size: 13px;
329
- text-align: center;
330
- }
331
- .pikaday-white table th abbr {
332
- text-decoration: none;
333
- }
334
- .pikaday-white table td {
335
- padding: 1px;
336
- }
337
- .pikaday-white table td button {
338
- width: 2.5em;
339
- height: 2.5em;
340
- text-align: center;
341
- @apply rounded-full;
342
- font-size: 13px;
343
- background-color: var(--backgroundColor);
344
- }
345
- .pikaday-white table td button:hover {
346
- @apply bg-slate-100;
347
- @apply text-slate-900;
348
- }
349
- .pikaday-white table td.is-today button {
350
- color: var(--currentDateTextColor);
351
- }
352
- .pikaday-white table td.is-selected button {
353
- @apply bg-primary-500;
354
- @apply text-white;
355
- @apply font-normal;
356
- }
357
-
358
- .pikaday-white table td button {
359
- color: var(--textColor);
297
+ .flatpickr-calendar .flatpickr-current-month {
298
+ padding-top: 5px;
360
299
  }
361
300
  </style>
@@ -171,7 +171,7 @@ export const WithDatePicker = (args) => ({
171
171
  <div class="btn btn-primary">Click me</div>
172
172
  </template>
173
173
  <template #dropdown>
174
- <div class="bg-white shadow-lg rounded border p-2 w-[260px]">
174
+ <div class="bg-white shadow-lg rounded border p-2">
175
175
  <BaseDatePicker v-model="date" inline></BaseDatePicker>
176
176
  </div>
177
177
  </template>
@@ -20,7 +20,7 @@ const Template = (args) => ({
20
20
  return { args };
21
21
  },
22
22
  template: `
23
- <form @submit.prevent="">
23
+ <form @submit.prevent="" class="mt-10">
24
24
  <BaseInputLabel v-bind="args"></BaseInputLabel>
25
25
  <BaseInput required name="name" placeholder="Enter your first name"></BaseInput>
26
26
  </form>
@@ -29,3 +29,8 @@ const Template = (args) => ({
29
29
 
30
30
  export const Demo = Template.bind({});
31
31
  Demo.args = {};
32
+
33
+ export const Help = Template.bind({});
34
+ Help.args = {
35
+ help: 'Enter your first name here',
36
+ };
@@ -1,13 +1,50 @@
1
1
  <template>
2
2
  <label :class="classes">
3
- {{ label }}<span v-if="required" class="text-red-600"> *</span>
3
+ <div
4
+ class="relative"
5
+ :class="[help ? 'cursor-help' : 'cursor-default']"
6
+ @mouseenter="showTooltip = true"
7
+ @mouseleave="showTooltip = false"
8
+ >
9
+ <span> {{ label }}</span>
10
+
11
+ <BaseIcon
12
+ v-if="help"
13
+ class="relative bottom-px ml-1 inline h-4 w-4 text-slate-400"
14
+ icon="heroicons:question-mark-circle-20-solid"
15
+ />
16
+
17
+ <span v-if="required" class="ml-0.5 text-red-600"> *</span>
18
+
19
+ <Transition
20
+ enter-active-class="transition duration-200 ease-out"
21
+ enter-from-class="transform scale-95 opacity-0"
22
+ enter-to-class="transform scale-100 opacity-100"
23
+ leave-active-class="transition duration-75 ease-in"
24
+ leave-from-class="transform scale-100 opacity-100"
25
+ leave-to-class="transform scale-95 opacity-0"
26
+ >
27
+ <div
28
+ v-if="showTooltip && help"
29
+ class="pointer-events-none absolute bottom-[100%] left-0 z-[1] w-auto max-w-[300px]"
30
+ >
31
+ <div
32
+ class="relative bottom-1 rounded-md bg-slate-700 py-1 px-2 text-xs leading-tight text-white"
33
+ >
34
+ {{ help }}
35
+ </div>
36
+ </div>
37
+ </Transition>
38
+ </div>
4
39
  </label>
5
40
  </template>
6
41
 
7
42
  <script lang="ts">
8
- import { defineComponent } from 'vue';
43
+ import { defineComponent, PropType } from 'vue';
44
+ import { BaseIcon } from '.';
9
45
 
10
46
  export default defineComponent({
47
+ components: { BaseIcon },
11
48
  props: {
12
49
  required: {
13
50
  default: false,
@@ -21,6 +58,15 @@ export default defineComponent({
21
58
  default: 'mb-1 block text-sm',
22
59
  type: String,
23
60
  },
61
+ help: {
62
+ default: null,
63
+ type: [String, null] as PropType<string | null>,
64
+ },
65
+ },
66
+ data() {
67
+ return {
68
+ showTooltip: false,
69
+ };
24
70
  },
25
71
  });
26
72
  </script>