sprintify-ui 0.6.78 → 0.6.80

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,376 @@
1
+ <template>
2
+ <BaseDropdown
3
+ :animated="true"
4
+ :keep-alive="false"
5
+ placement="bottom-end"
6
+ >
7
+ <template #button>
8
+ <div class="relative">
9
+ <div
10
+ :class="iconWrapClasses"
11
+ >
12
+ <BaseIcon
13
+ :class="iconClasses"
14
+ icon="heroicons:clock"
15
+ />
16
+ </div>
17
+ <input
18
+ v-model="modelValueFormatted"
19
+ type="text"
20
+ :name="nameInternal"
21
+ :disabled="disabled"
22
+ class="w-full block"
23
+ :class="classes"
24
+ :placeholder="placeholder"
25
+ >
26
+ <div
27
+ v-if="modelValueFormatted && !disabled"
28
+ :class="closeWrapClasses"
29
+ >
30
+ <button
31
+ type="button"
32
+ class="flex justify-center items-center rounded hover:bg-slate-100 aspect-1"
33
+ @click="clear()"
34
+ >
35
+ <BaseIcon
36
+ :class="iconClasses"
37
+ icon="heroicons:x-mark"
38
+ />
39
+ </button>
40
+ </div>
41
+ </div>
42
+ </template>
43
+
44
+ <template #dropdown="{ close }">
45
+ <div
46
+ class="inline-block w-[200px] overflow-hidden input-rounded ring-1 ring-black ring-opacity-10 bg-white py-2 shadow-2xl"
47
+ >
48
+ <div class="text-sm font-normal">
49
+ <div class="flex base-time-picker">
50
+ <!-- Hours -->
51
+ <div
52
+ ref="hoursContainer"
53
+ class="grow overflow-y-auto h-48 border-r pl-[8px]"
54
+ >
55
+ <BaseScrollColumn
56
+ :value="modelValue"
57
+ element="hourElements"
58
+ :container="hoursContainer"
59
+ :times="hours"
60
+ type="hour"
61
+ @select-time="selectTime"
62
+ />
63
+
64
+ <div class="h-40" />
65
+ </div>
66
+
67
+ <!-- Minutes -->
68
+ <div
69
+ ref="minutesContainer"
70
+ class="grow overflow-y-auto h-48 border-r pl-[8px]"
71
+ >
72
+ <BaseScrollColumn
73
+ :value="modelValue"
74
+ element="minuteElements"
75
+ :container="minutesContainer"
76
+ :times="minutes"
77
+ type="minute"
78
+ @select-time="selectTime"
79
+ />
80
+
81
+ <div class="h-40" />
82
+ </div>
83
+
84
+ <!-- Seconds -->
85
+ <div
86
+ v-if="!disabledSeconds"
87
+ ref="secondsContainer"
88
+ class="grow overflow-y-auto h-48 pl-[8px]"
89
+ >
90
+ <BaseScrollColumn
91
+ :value="modelValue"
92
+ element="secondElements"
93
+ :container="secondsContainer"
94
+ :times="seconds"
95
+ type="second"
96
+ @select-time="selectTime"
97
+ />
98
+
99
+ <div class="h-40" />
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Buttons Action -->
104
+ <div class="flex justify-between items-center pt-2 mt-1 px-2 border-t">
105
+ <button
106
+ class="btn btn-slate btn-sm block"
107
+ @click="now(close)"
108
+ >
109
+ {{ t('sui.now') }}
110
+ </button>
111
+
112
+ <button
113
+ class="btn btn-primary btn-sm block"
114
+ @click="ok(close)"
115
+ >
116
+ {{ t('sui.ok') }}
117
+ </button>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </template>
122
+ </BaseDropdown>
123
+ </template>
124
+
125
+ <script lang="ts" setup>
126
+ import { PropType } from 'vue';
127
+ import { Icon as BaseIcon } from '@iconify/vue';
128
+ import { useField } from '@/composables/field';
129
+
130
+ import { t } from '@/i18n';
131
+ import { twMerge } from 'tailwind-merge';
132
+ import { Size, sizes } from '@/utils/sizes';
133
+ import BaseDropdown from './BaseDropdown.vue';
134
+ import BaseScrollColumn from './BaseScrollColumn.vue';
135
+ import { DateTime } from 'luxon';
136
+
137
+ const props = defineProps({
138
+ modelValue: {
139
+ default: undefined,
140
+ type: [String, null, undefined] as PropType<string | null | undefined>,
141
+ },
142
+ size: {
143
+ default: undefined,
144
+ type: String as PropType<Size>,
145
+ },
146
+ class: {
147
+ default: '',
148
+ type: [String, Array] as PropType<string | string[]>,
149
+ },
150
+ disabled: {
151
+ default: false,
152
+ type: Boolean,
153
+ },
154
+ name: {
155
+ default: '',
156
+ type: String,
157
+ },
158
+ placeholder: {
159
+ default: t('sui.click_or_select_time'),
160
+ type: String,
161
+ },
162
+ required: {
163
+ default: true,
164
+ type: Boolean,
165
+ },
166
+ hourStep: {
167
+ default: 1,
168
+ type: Number,
169
+ },
170
+ minuteStep: {
171
+ default: 1,
172
+ type: Number,
173
+ },
174
+ secondStep: {
175
+ default: 1,
176
+ type: Number,
177
+ },
178
+ disabledSeconds: {
179
+ default: false,
180
+ type: Boolean,
181
+ },
182
+ hasError: {
183
+ default: false,
184
+ type: Boolean,
185
+ },
186
+ });
187
+
188
+ const emit = defineEmits(['update:modelValue']);
189
+
190
+ const { nameInternal, hasErrorInternal, emitUpdate, sizeInternal } =
191
+ useField({
192
+ name: computed(() => props.name),
193
+ required: computed(() => props.required),
194
+ size: computed(() => props.size),
195
+ hasError: computed(() => props.hasError),
196
+ emit: emit,
197
+ });
198
+
199
+ const timeRegex = computed(() => {
200
+ if (props.disabledSeconds) {
201
+ return new RegExp(
202
+ `^([01]?[0-9]|2[0-3]):([0-5][0-9])$`
203
+ );
204
+ }
205
+
206
+ return new RegExp(
207
+ `^([01]?[0-9]|2[0-3]):([0-5][0-9])(:([0-5][0-9]))?$`
208
+ );
209
+ });
210
+ // const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/;
211
+ const modelValueFormatted = computed((): string | string[] | null => {
212
+ if (!props.modelValue) {
213
+ return null;
214
+ }
215
+
216
+ if (!timeRegex.value.test(props.modelValue)) {
217
+ return null;
218
+ }
219
+
220
+ return props.modelValue;
221
+ });
222
+
223
+ const classes = computed(() => {
224
+ const base = 'transition-colors duration-200 input-rounded';
225
+ const focus = 'focus:input-focus'
226
+ const error = hasErrorInternal.value ? 'border-red-500 focus:input-focus-error' : 'border-slate-300';
227
+ const disabled = 'disabled:cursor-not-allowed disabled:text-slate-300';
228
+ const sizeConfig = sizes[sizeInternal.value];
229
+ const padding = {
230
+ 'xs': 'pl-[1.54rem] pr-5',
231
+ 'sm': 'pl-[2.1rem] pr-6',
232
+ 'md': 'pl-10 pr-7',
233
+ }[sizeInternal.value];
234
+
235
+ const paddingRight = props.modelValue ? padding : 'pr-0';
236
+
237
+ return twMerge(
238
+ base,
239
+ focus,
240
+ error,
241
+ disabled,
242
+ padding,
243
+ paddingRight,
244
+ sizeConfig.height,
245
+ sizeConfig.fontSize,
246
+ );
247
+ });
248
+
249
+ const closeWrapClasses = computed(() => {
250
+ const base = 'absolute top-0 right-0 flex justify-center p-1';
251
+ const sizeConfig = sizes[sizeInternal.value];
252
+
253
+ return twMerge(
254
+ base,
255
+ sizeConfig.height,
256
+ );
257
+ });
258
+
259
+ const iconWrapClasses = computed(() => {
260
+ const base = 'pointer-events-none absolute top-0 left-0 flex items-center justify-center';
261
+ const sizeConfig = sizes[sizeInternal.value];
262
+ const padding = {
263
+ 'xs': 'pl-2',
264
+ 'sm': 'pl-2.5',
265
+ 'md': 'pl-3',
266
+ }[sizeInternal.value];
267
+
268
+ return twMerge(
269
+ base,
270
+ sizeConfig.height,
271
+ padding
272
+ );
273
+ });
274
+
275
+ const iconClasses = computed(() => {
276
+ const base = '';
277
+ const error = hasErrorInternal.value ? 'text-red-500' : 'text-slate-500';
278
+ const sizeConfig = sizes[sizeInternal.value];
279
+
280
+ return twMerge(
281
+ base,
282
+ error,
283
+ sizeConfig.iconSize,
284
+ );
285
+ });
286
+
287
+ function clear() {
288
+ emitUpdate(null);
289
+ clearSelectedValues();
290
+ }
291
+
292
+ /**
293
+ * Time picker process begin
294
+ */
295
+
296
+ const hoursContainer = ref<HTMLElement | null>(null);
297
+
298
+ const minutesContainer = ref<HTMLElement | null>(null);
299
+
300
+ const secondsContainer = ref<HTMLElement | null>(null);
301
+
302
+ const hourValue = ref('' as string | null);
303
+ const minuteValue = ref('' as string | null);
304
+ const secondValue = ref('' as string | null);
305
+
306
+ const generateValues = (length: number, step: number) => {
307
+ return Array.from({ length }, (_, i) => i)
308
+ .filter(value => value % step === 0)
309
+ .map(value => value < 10 ? `0${value}` : `${value}`);
310
+ };
311
+
312
+ const hours = computed(() => generateValues(24, props.hourStep));
313
+ const minutes = computed(() => generateValues(60, props.minuteStep));
314
+ const seconds = computed(() => generateValues(60, props.secondStep));
315
+
316
+ function selectTime(time: string, type: string) {
317
+
318
+ if (type === 'hour') {
319
+ hourValue.value = time;
320
+ }
321
+ if (type === 'minute') {
322
+ minuteValue.value = time;
323
+ }
324
+ if (type === 'second') {
325
+ secondValue.value = time;
326
+ }
327
+
328
+ if (props.disabledSeconds) {
329
+ secondValue.value = '00';
330
+ }
331
+ }
332
+
333
+ function ok(close: () => void) {
334
+ let time = `${hourValue.value}:${minuteValue.value}:${secondValue.value}`;
335
+
336
+ if (props.disabledSeconds) {
337
+ time = `${hourValue.value}:${minuteValue.value}`;
338
+ }
339
+
340
+ if (!timeRegex.value.test(time)) {
341
+ return null;
342
+ }
343
+
344
+ emitUpdate(time);
345
+
346
+ console.log('time', time);
347
+
348
+
349
+ close();
350
+ }
351
+
352
+ function now(close: () => void) {
353
+ const now = DateTime.now();
354
+
355
+ hourValue.value = now.hour.toString().padStart(2, '0');
356
+ minuteValue.value = now.minute.toString().padStart(2, '0');
357
+ secondValue.value = now.second.toString().padStart(2, '0');
358
+
359
+ let time = `${hourValue.value}:${minuteValue.value}:${secondValue.value}`;
360
+
361
+ if (props.disabledSeconds) {
362
+ time = `${hourValue.value}:${minuteValue.value}`;
363
+ }
364
+
365
+ emitUpdate(time);
366
+
367
+ close();
368
+ }
369
+
370
+ const clearSelectedValues = () => {
371
+ hourValue.value = null;
372
+ minuteValue.value = null;
373
+ secondValue.value = null;
374
+ };
375
+
376
+ </script>
@@ -28,6 +28,7 @@ import BaseCropperModal from './BaseCropperModal.vue';
28
28
  import BaseDataIterator from './BaseDataIterator.vue';
29
29
  import BaseDataTable from './BaseDataTable.vue';
30
30
  import BaseDatePicker from './BaseDatePicker.vue';
31
+ import BaseTimePicker from './BaseTimePicker.vue';
31
32
  import BaseDateSelect from './BaseDateSelect.vue';
32
33
  import BaseDescriptionList from './BaseDescriptionList.vue';
33
34
  import BaseDescriptionListItem from './BaseDescriptionListItem.vue';
@@ -133,6 +134,7 @@ export {
133
134
  BaseDataIterator,
134
135
  BaseDataTable,
135
136
  BaseDatePicker,
137
+ BaseTimePicker,
136
138
  BaseDateSelect,
137
139
  BaseDescriptionList,
138
140
  BaseDescriptionListItem,
package/src/lang/en.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "month": "Month",
8
8
  "save": "Save",
9
9
  "sui": {
10
+ "notes": "Notes",
10
11
  "address": "Address",
11
12
  "address_1_placeholder": "Postal address",
12
13
  "address_2_description": "Apartment, suite, unit, building",
@@ -20,6 +21,7 @@
20
21
  "city": "City",
21
22
  "clear": "Clear",
22
23
  "click_or_select_date": "Click or select date",
24
+ "click_or_select_time": "Enter time (HH:mm:ss)",
23
25
  "click_to_copy": "Click to copy",
24
26
  "columns": "Columns",
25
27
  "confirm": "Confirm",
@@ -50,7 +52,9 @@
50
52
  "none": "None",
51
53
  "nothing_found": "Nothing found",
52
54
  "notifications_empty": "You have no new notifications",
55
+ "now": "Now",
53
56
  "of": "of",
57
+ "ok": "Ok",
54
58
  "or": "or",
55
59
  "page": "Page",
56
60
  "pagination_detail_1": "Viewing",
@@ -92,4 +96,4 @@
92
96
  "today": "Today",
93
97
  "week": "Week",
94
98
  "year": "Year"
95
- }
99
+ }
package/src/lang/fr.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "month": "Mois",
8
8
  "save": "Sauvegarder",
9
9
  "sui": {
10
+ "notes": "Notes",
10
11
  "address": "Adresse",
11
12
  "address_1_placeholder": "Adresse postale",
12
13
  "address_2_description": "Appartement, suite, unité, immeuble",
@@ -20,6 +21,7 @@
20
21
  "city": "Ville",
21
22
  "clear": "Effacer",
22
23
  "click_or_select_date": "Cliquer ou sélectionner une date",
24
+ "click_or_select_time": "Entrez l'heure (HH:mm:ss)",
23
25
  "click_to_copy": "Cliquer pour copier",
24
26
  "columns": "Colonnes",
25
27
  "confirm": "Confirmer",
@@ -50,7 +52,9 @@
50
52
  "none": "Aucun",
51
53
  "nothing_found": "Rien n'a été trouvé",
52
54
  "notifications_empty": "Vous n'avez aucune nouvelle notification",
55
+ "now": "Maintenant",
53
56
  "of": "de",
57
+ "ok": "Appliquer",
54
58
  "or": "ou",
55
59
  "page": "Page",
56
60
  "pagination_detail_1": "Affichage de",
@@ -92,4 +96,4 @@
92
96
  "today": "Aujourd\\'hui",
93
97
  "week": "Semaine",
94
98
  "year": "Année"
95
- }
99
+ }
@@ -27,6 +27,14 @@
27
27
  >
28
28
  <BaseDatePicker v-model="date" />
29
29
  </BaseField>
30
+
31
+ <BaseField
32
+ :size="size"
33
+ class="min-w-[200px]"
34
+ >
35
+ <BaseTimePicker v-model="date" />
36
+ </BaseField>
37
+
30
38
  <BaseField
31
39
  :size="size"
32
40
  >
@@ -147,6 +155,23 @@
147
155
  :size="size"
148
156
  />
149
157
  </div>
158
+
159
+ <div
160
+ v-for="size in sizes"
161
+ :key="size"
162
+ class="flex flex-col gap-3"
163
+ >
164
+ <BaseInput
165
+ v-model="text"
166
+ :size="size"
167
+ placeholder="Name"
168
+ icon-left="heroicons:calendar"
169
+ />
170
+ <BaseTimePicker
171
+ v-model="date"
172
+ :size="size"
173
+ />
174
+ </div>
150
175
  </div>
151
176
  </template>
152
177
 
@@ -168,6 +193,7 @@ import BaseInputPercent from '@/components/BaseInputPercent.vue';
168
193
  import BaseTextareaAutoresize from '@/components/BaseTextareaAutoresize.vue';
169
194
  import BaseDatePicker from '@/components/BaseDatePicker.vue';
170
195
  import BaseDateSelect from '@/components/BaseDateSelect.vue';
196
+ import BaseTimePicker from '@/components/BaseTimePicker.vue';
171
197
 
172
198
  const sizes = ref<Size[]>(['xs', 'sm', 'md']);
173
199