sprintify-ui 0.0.183 → 0.0.184

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.
Files changed (49) hide show
  1. package/dist/sprintify-ui.es.js +16888 -14992
  2. package/dist/style.css +1 -1
  3. package/dist/types/src/components/BaseCropper.vue.d.ts +57 -0
  4. package/dist/types/src/components/BaseCropperModal.vue.d.ts +27 -0
  5. package/dist/types/src/components/BaseDisplayRelativeTime.vue.d.ts +3 -3
  6. package/dist/types/src/components/BaseFilePicker.vue.d.ts +52 -37
  7. package/dist/types/src/components/BaseFilePickerCrop.vue.d.ts +57 -0
  8. package/dist/types/src/components/BaseFileUploader.vue.d.ts +65 -81
  9. package/dist/types/src/components/BaseMediaLibrary.vue.d.ts +20 -10
  10. package/dist/types/src/components/BaseTableColumn.vue.d.ts +1 -1
  11. package/dist/types/src/components/index.d.ts +4 -1
  12. package/dist/types/src/index.d.ts +24 -4
  13. package/dist/types/src/svg/BaseEmptyState.vue.d.ts +1 -1
  14. package/dist/types/src/types/ImagePickerResult.d.ts +5 -0
  15. package/dist/types/src/types/index.d.ts +28 -0
  16. package/dist/types/src/utils/blob.d.ts +3 -0
  17. package/dist/types/src/utils/cropper/avatar.d.ts +5 -0
  18. package/dist/types/src/utils/cropper/cover.d.ts +5 -0
  19. package/dist/types/src/utils/cropper/presetInterface.d.ts +7 -0
  20. package/dist/types/src/utils/cropper/presets.d.ts +6 -0
  21. package/dist/types/src/utils/fileValidations.d.ts +2 -0
  22. package/dist/types/src/utils/index.d.ts +3 -1
  23. package/dist/types/src/utils/resizeImageFromURI.d.ts +1 -0
  24. package/package.json +35 -32
  25. package/src/components/BaseCropper.stories.js +113 -0
  26. package/src/components/BaseCropper.vue +451 -0
  27. package/src/components/BaseCropperModal.stories.js +54 -0
  28. package/src/components/BaseCropperModal.vue +139 -0
  29. package/src/components/BaseFilePicker.stories.js +30 -3
  30. package/src/components/BaseFilePicker.vue +107 -75
  31. package/src/components/BaseFilePickerCrop.stories.js +134 -0
  32. package/src/components/BaseFilePickerCrop.vue +116 -0
  33. package/src/components/BaseFileUploader.stories.js +11 -7
  34. package/src/components/BaseFileUploader.vue +57 -86
  35. package/src/components/BaseMediaLibrary.stories.js +24 -5
  36. package/src/components/BaseMediaLibrary.vue +17 -2
  37. package/src/components/index.ts +6 -0
  38. package/src/lang/en.json +6 -1
  39. package/src/lang/fr.json +6 -1
  40. package/src/types/ImagePickerResult.ts +5 -0
  41. package/src/types/index.ts +31 -0
  42. package/src/utils/blob.ts +30 -0
  43. package/src/utils/cropper/avatar.ts +33 -0
  44. package/src/utils/cropper/cover.ts +41 -0
  45. package/src/utils/cropper/presetInterface.ts +16 -0
  46. package/src/utils/cropper/presets.ts +7 -0
  47. package/src/utils/fileValidations.ts +26 -0
  48. package/src/utils/index.ts +12 -1
  49. package/src/utils/resizeImageFromURI.ts +118 -0
@@ -0,0 +1,113 @@
1
+ import BaseCropper from '@/components/BaseCropper.vue';
2
+ import BaseLoadingCover from '@/components/BaseLoadingCover.vue';
3
+ import BaseAppNotifications from '@/components/BaseAppNotifications.vue';
4
+ import { Icon as BaseIcon } from '@iconify/vue';
5
+
6
+ export default {
7
+ title: 'Form/BaseCropper',
8
+ component: BaseCropper,
9
+ args: {
10
+ source:
11
+ 'https://images.unsplash.com/photo-1560250097-0b93528c311a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&q=80',
12
+ },
13
+ };
14
+
15
+ const Template = (args) => ({
16
+ components: {
17
+ BaseCropper,
18
+ BaseIcon,
19
+ BaseLoadingCover,
20
+ BaseAppNotifications,
21
+ },
22
+ setup() {
23
+ const cropperRef = ref(null);
24
+
25
+ function saveAsCanvas() {
26
+ cropperRef.value.save({
27
+ type: 'canvas',
28
+ size: 'original',
29
+ });
30
+ }
31
+
32
+ return { args, saveAsCanvas, cropperRef };
33
+ },
34
+ template: `
35
+ <BaseCropper ref="cropperRef" v-bind="args">
36
+ <template #footer="{cancelCrop, initializing, rotateLeft, rotateRight}">
37
+ <div class="mt-1 flex flex-wrap gap-2">
38
+ <button
39
+ type="button"
40
+ :disabled="initializing"
41
+ class="btn btn-sm"
42
+ @click="rotateLeft()"
43
+ >
44
+ Rotate Left
45
+ </button>
46
+
47
+ <button
48
+ type="button"
49
+ :disabled="initializing"
50
+ class="btn btn-sm"
51
+ @click="rotateRight()"
52
+ >
53
+ Rotate Right
54
+ </button>
55
+
56
+ <button
57
+ type="button"
58
+ :disabled="!initializing"
59
+ class="btn btn-sm"
60
+ @click="cancelCrop()"
61
+ >
62
+ Cancel
63
+ </button>
64
+
65
+ <button
66
+ type="button"
67
+ :disabled="initializing"
68
+ class="btn btn-sm"
69
+ @click="saveAsCanvas()"
70
+ >
71
+ Save as Canvas
72
+ </button>
73
+ </div>
74
+ </template>
75
+ </BaseCropper>
76
+
77
+ <BaseAppNotifications></BaseAppNotifications>
78
+ `,
79
+ });
80
+
81
+ export const Demo = Template.bind({});
82
+
83
+ Demo.args = {
84
+ config: {
85
+ height: 200,
86
+ width: 200,
87
+ viewport: {
88
+ width: 200,
89
+ height: 200,
90
+ type: 'rectangle',
91
+ },
92
+ boundary: {
93
+ width: 200,
94
+ height: 200,
95
+ },
96
+ },
97
+ };
98
+
99
+ export const Avatar = Template.bind({});
100
+
101
+ Avatar.args = {
102
+ preset: 'avatar',
103
+ };
104
+
105
+ export const Cover = Template.bind({});
106
+
107
+ Cover.args = {
108
+ preset: 'cover',
109
+ presetOptions: {
110
+ size: 400,
111
+ ratio: 16 / 9,
112
+ },
113
+ };
@@ -0,0 +1,451 @@
1
+ <template>
2
+ <div
3
+ class="relative"
4
+ :style="{
5
+ width: cropperConfiguration?.boundary?.width + 'px',
6
+ }"
7
+ >
8
+ <slot
9
+ name="header"
10
+ :saving="saving"
11
+ :initializing="initializing"
12
+ v-bind="shared"
13
+ />
14
+
15
+ <div ref="container" class="base-cropper-wrapper relative">
16
+ <div
17
+ :style="{
18
+ width: cropperConfiguration?.boundary?.width + 'px',
19
+ height: cropperConfiguration?.showZoomer
20
+ ? (cropperConfiguration?.boundary?.height ?? 0) +
21
+ ZOOMER_HEIGHT +
22
+ 'px'
23
+ : cropperConfiguration?.boundary?.height + 'px',
24
+ }"
25
+ >
26
+ <div
27
+ ref="croppie"
28
+ :style="{
29
+ visibility: initializing ? 'hidden' : 'visible',
30
+ }"
31
+ />
32
+ </div>
33
+
34
+ <div
35
+ v-show="!initializing"
36
+ class="absolute left-0 z-[1] flex w-full items-center justify-center"
37
+ :style="{
38
+ bottom: cropperConfiguration?.showZoomer
39
+ ? ZOOMER_HEIGHT - 13 + 'px'
40
+ : 6 + 'px',
41
+ }"
42
+ >
43
+ <div class="flex overflow-hidden rounded-full shadow-md">
44
+ <button
45
+ type="button"
46
+ :disabled="disabled"
47
+ class="border-r border-slate-300 bg-white px-3 py-1.5 hover:bg-slate-100"
48
+ @click="rotateLeft"
49
+ >
50
+ <BaseIcon icon="mdi:rotate-left" class="h-4 w-4" />
51
+ </button>
52
+ <button
53
+ type="button"
54
+ :disabled="disabled"
55
+ class="bg-white px-3 py-1.5 hover:bg-slate-100"
56
+ @click="rotateRight"
57
+ >
58
+ <BaseIcon icon="mdi:rotate-right" class="h-4 w-4" />
59
+ </button>
60
+ </div>
61
+ </div>
62
+
63
+ <div
64
+ v-if="showDragHelp"
65
+ class="pointer-events-none absolute left-0 top-14 z-[1] flex w-full animate-pulse justify-center"
66
+ >
67
+ <div
68
+ class="flex items-center rounded-lg bg-black bg-opacity-75 px-3 py-1.5 text-center text-white"
69
+ >
70
+ <BaseIcon icon="ri:drag-move-2-fill" class="mr-1 h-5 w-5" />
71
+ <span>
72
+ {{ $t('drag_to_reposition') }}
73
+ </span>
74
+ </div>
75
+ </div>
76
+
77
+ <BaseLoadingCover
78
+ :delay="40"
79
+ class="z-[1]"
80
+ :model-value="initializing"
81
+ ></BaseLoadingCover>
82
+ </div>
83
+
84
+ <slot
85
+ name="footer"
86
+ :saving="saving"
87
+ :initializing="initializing"
88
+ v-bind="shared"
89
+ />
90
+ </div>
91
+ </template>
92
+
93
+ <script lang="ts" setup>
94
+ import { onBeforeUnmount, Ref } from 'vue';
95
+ import Croppie, { CroppieOptions, CropType, ResultOptions } from 'croppie';
96
+ import 'croppie/croppie.css';
97
+ import { ref, onMounted } from 'vue';
98
+ import { resizeImageFromURI } from '@/utils';
99
+ import { cloneDeep, debounce } from 'lodash';
100
+ import { CropperConfig } from '../types';
101
+ import { BaseIcon, BaseLoadingCover } from '.';
102
+ import { presets } from '@/utils/cropper/presets';
103
+
104
+ const props = defineProps<{
105
+ source: string;
106
+ config?: CropperConfig;
107
+ preset?: 'avatar' | 'cover';
108
+ presetOptions?: Record<string, any>;
109
+ disabled?: boolean;
110
+ saveOptions?: ResultOptions;
111
+ }>();
112
+
113
+ const RESIZE_MAX_SIZE = 1000;
114
+ const ZOOMER_HEIGHT = 44;
115
+
116
+ const container = ref(null) as Ref<HTMLElement | null>;
117
+ const croppie = ref(null) as Ref<HTMLElement | null>;
118
+
119
+ let lastSource = null as string | null;
120
+ let lastInitialResize = null as number | null;
121
+ let lastSourceResized = null as string | null;
122
+
123
+ let cropper = null as Croppie | null;
124
+
125
+ // Croppie initialization
126
+ const initializing = ref(false);
127
+
128
+ // Croppie saving cropped image
129
+ const saving = ref(false);
130
+
131
+ const showDragHelp = ref(false);
132
+
133
+ const sourceOriginalWidth = ref(0);
134
+ const sourceOriginalHeight = ref(0);
135
+
136
+ const resetViewPortDebounced = debounce(() => {
137
+ resetViewPort();
138
+ }, 100);
139
+
140
+ watch(
141
+ () =>
142
+ JSON.stringify({
143
+ ...(props.config ?? {}),
144
+ source: props.source,
145
+ preset: props.preset,
146
+ presetOptions: props.presetOptions,
147
+ }),
148
+ () => {
149
+ resetViewPortDebounced();
150
+ }
151
+ );
152
+
153
+ onMounted(() => {
154
+ init();
155
+ });
156
+
157
+ onBeforeUnmount(() => {
158
+ // Give time for animations to finish
159
+ setTimeout(() => {
160
+ destroy();
161
+ }, 400);
162
+ });
163
+
164
+ const cropperConfiguration = computed<CroppieOptions>(() => {
165
+ let config = cloneDeep(props.config ?? {});
166
+
167
+ if (props.preset) {
168
+ const preset = presets[props.preset] ?? null;
169
+
170
+ if (preset) {
171
+ const presetClass = new preset(config, props.presetOptions);
172
+ config = presetClass.handle();
173
+ }
174
+ }
175
+
176
+ // Put default values
177
+
178
+ config.width = config.width ?? 300;
179
+ config.height = config.height ?? 300;
180
+ config.maxWidth = config.maxWidth ?? undefined;
181
+ config.enableResize = config.enableResize ?? false;
182
+ config.enableZoom = config.enableZoom ?? true;
183
+ config.enableOrientation = config.enableOrientation ?? true;
184
+ config.showZoomer = config.showZoomer ?? true;
185
+ config.viewport = config.viewport ?? {
186
+ width: config.width,
187
+ height: config.height,
188
+ type: 'square' as CropType,
189
+ };
190
+ config.boundary = config.boundary ?? {
191
+ width: config.width,
192
+ height: config.height,
193
+ };
194
+
195
+ // Make sure values respect max width
196
+
197
+ if (config.maxWidth) {
198
+ const ratio = (config.width as number) / (config.height as number);
199
+ config.width = Math.min(config.width as number, config.maxWidth);
200
+ config.height = config.width / ratio;
201
+ config.boundary.width = Math.min(config.boundary.width, config.maxWidth);
202
+ config.boundary.height = config.boundary.width / ratio;
203
+ config.viewport.width = Math.min(config.viewport.width, config.maxWidth);
204
+ config.viewport.height = config.viewport.width / ratio;
205
+ }
206
+
207
+ return {
208
+ enableExif: true,
209
+ enableResize: config.enableResize,
210
+ enableZoom: config.enableZoom,
211
+ enableOrientation: config.enableOrientation,
212
+ showZoomer: config.showZoomer,
213
+ viewport: config.viewport,
214
+ boundary: config.boundary,
215
+ };
216
+ });
217
+
218
+ function init() {
219
+ console.log('init');
220
+
221
+ if (initializing.value) {
222
+ return;
223
+ }
224
+
225
+ // Start the loading state...
226
+ initializing.value = true;
227
+
228
+ // ...Give time to the loading state to render before doing CPU intensive tasks
229
+ setTimeout(async () => {
230
+ try {
231
+ await initCropper();
232
+ } catch (error) {
233
+ console.error(error);
234
+ } finally {
235
+ initializing.value = false;
236
+ }
237
+ }, 10);
238
+ }
239
+
240
+ async function initCropper() {
241
+ if (croppie.value == null) {
242
+ throw new Error('Croppie element not found');
243
+ }
244
+
245
+ cropper = new Croppie(croppie.value, cropperConfiguration.value);
246
+
247
+ const sourceWasChanged = lastSource != props.source;
248
+ const initialResizeWasChanged =
249
+ lastInitialResize != props.config?.initialResize;
250
+
251
+ if (sourceWasChanged || initialResizeWasChanged) {
252
+ // Try to resize image to avoid using too much memory
253
+ lastSourceResized = await resizeImage(props.source);
254
+ }
255
+
256
+ lastSource = props.source;
257
+ lastInitialResize = props.config?.initialResize ?? null;
258
+
259
+ lastSourceResized = lastSourceResized ?? props.source;
260
+
261
+ await cropper.bind({
262
+ url: lastSourceResized,
263
+ zoom: 0,
264
+ });
265
+
266
+ showDragHelp.value = true;
267
+
268
+ croppie.value.addEventListener('update', (a) => {
269
+ if (initializing.value) {
270
+ return;
271
+ }
272
+
273
+ showDragHelp.value = false;
274
+ });
275
+ }
276
+
277
+ async function resizeImage(url: string): Promise<string> {
278
+ console.log('resizeImage');
279
+
280
+ await getOriginalDimensions();
281
+
282
+ try {
283
+ const initialResize = getInitialResize();
284
+
285
+ if (!initialResize) {
286
+ return url;
287
+ }
288
+
289
+ return await resizeImageFromURI(
290
+ url,
291
+ initialResize.height,
292
+ initialResize.width
293
+ );
294
+ } catch (e: any) {
295
+ return url;
296
+ }
297
+ }
298
+
299
+ async function save(
300
+ result?: ResultOptions
301
+ ): Promise<HTMLCanvasElement | string | Blob | null> {
302
+ if (!cropper) {
303
+ return null;
304
+ }
305
+
306
+ saving.value = true;
307
+
308
+ const resultConfig = result ??
309
+ props.saveOptions ?? {
310
+ type: 'blob',
311
+ size: 'original',
312
+ circle: false,
313
+ };
314
+
315
+ console.log(resultConfig);
316
+
317
+ const r = await cropper.result(resultConfig);
318
+
319
+ saving.value = false;
320
+
321
+ return r;
322
+ }
323
+
324
+ function resetViewPort() {
325
+ if (!initializing.value) {
326
+ destroy();
327
+ init();
328
+ }
329
+ }
330
+
331
+ function rotateLeft() {
332
+ cropper?.rotate(90);
333
+ }
334
+
335
+ function rotateRight() {
336
+ cropper?.rotate(-90);
337
+ }
338
+
339
+ const destroy = () => {
340
+ cropper?.destroy();
341
+ };
342
+
343
+ async function getOriginalDimensions() {
344
+ return new Promise((resolve) => {
345
+ const img = new Image();
346
+ img.src = props.source;
347
+ img.onload = () => {
348
+ sourceOriginalWidth.value = img.width;
349
+ sourceOriginalHeight.value = img.height;
350
+ resolve(true);
351
+ };
352
+ });
353
+ }
354
+
355
+ function getInitialResize() {
356
+ const resizeMax = props.config?.initialResize ?? RESIZE_MAX_SIZE;
357
+
358
+ const reducedWidth =
359
+ sourceOriginalWidth.value > resizeMax
360
+ ? resizeMax
361
+ : sourceOriginalWidth.value;
362
+
363
+ const reducedHeight =
364
+ sourceOriginalHeight.value > resizeMax
365
+ ? resizeMax
366
+ : sourceOriginalHeight.value;
367
+
368
+ const ratioWidth = reducedWidth / sourceOriginalWidth.value;
369
+ const ratioHeight = reducedHeight / sourceOriginalHeight.value;
370
+ const ratio = ratioWidth > ratioHeight ? ratioWidth : ratioHeight;
371
+
372
+ return {
373
+ width: sourceOriginalWidth.value * ratio,
374
+ height: sourceOriginalHeight.value * ratio,
375
+ };
376
+ }
377
+
378
+ const shared = {
379
+ save,
380
+ resetViewPort,
381
+ rotateLeft,
382
+ rotateRight,
383
+ };
384
+
385
+ defineExpose(shared);
386
+ </script>
387
+
388
+ <style lang="postcss">
389
+ .base-cropper-wrapper {
390
+ .cr-slider-wrap {
391
+ margin-top: 20px;
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+
396
+ &:before {
397
+ content: '-';
398
+ flex-shrink: 0;
399
+ font-size: 20px;
400
+ font-weight: 400;
401
+ @apply text-slate-900;
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: center;
405
+ width: 36px;
406
+ height: 24px;
407
+ }
408
+
409
+ &:after {
410
+ content: '+';
411
+ flex-shrink: 0;
412
+ font-size: 20px;
413
+ font-weight: 400;
414
+ @apply text-slate-900;
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: center;
418
+ width: 36px;
419
+ height: 24px;
420
+ }
421
+ }
422
+ .cr-slider::-webkit-slider-runnable-track {
423
+ @apply bg-gray-300;
424
+ height: 5px;
425
+ }
426
+ .cr-slider::-moz-range-track {
427
+ @apply bg-gray-300;
428
+ height: 5px;
429
+ }
430
+ .cr-slider::-webkit-slider-thumb {
431
+ width: 24px;
432
+ height: 24px;
433
+ cursor: pointer;
434
+ position: relative;
435
+ top: -3px;
436
+ @apply bg-white;
437
+ @apply shadow-md;
438
+ @apply ring-0;
439
+ }
440
+ .cr-slider::-moz-range-thumb {
441
+ width: 24px;
442
+ height: 24px;
443
+ cursor: pointer;
444
+ position: relative;
445
+ top: -3px;
446
+ @apply bg-white;
447
+ @apply shadow-md;
448
+ @apply ring-0;
449
+ }
450
+ }
451
+ </style>
@@ -0,0 +1,54 @@
1
+ import BaseCropperModal from '@/components/BaseCropperModal.vue';
2
+ import BaseAppNotifications from '@/components/BaseAppNotifications.vue';
3
+
4
+ const source =
5
+ 'https://images.unsplash.com/photo-1560250097-0b93528c311a?auto=format&fit=crop&q=80';
6
+
7
+ export default {
8
+ title: 'Form/BaseCropperModal',
9
+ component: BaseCropperModal,
10
+ args: {},
11
+ };
12
+
13
+ const Template = (args) => ({
14
+ components: {
15
+ BaseCropperModal,
16
+ BaseAppNotifications,
17
+ },
18
+ setup() {
19
+ const value = ref(true);
20
+ return { args, value };
21
+ },
22
+ template: `
23
+ <BaseCropperModal v-bind="args" v-model="value"></BaseCropperModal>
24
+ <BaseAppNotifications></BaseAppNotifications>
25
+ `,
26
+ });
27
+
28
+ export const Avatar = Template.bind({});
29
+ Avatar.args = {
30
+ cropper: {
31
+ source: source,
32
+ config: {
33
+ maxWidth: 300,
34
+ },
35
+ preset: 'avatar',
36
+ presetOptions: {
37
+ size: 300,
38
+ },
39
+ },
40
+ };
41
+
42
+ export const Cover = Template.bind({});
43
+ Cover.args = {
44
+ cropper: {
45
+ source: source,
46
+ config: {
47
+ maxWidth: 600,
48
+ },
49
+ preset: 'cover',
50
+ presetOptions: {
51
+ size: 600,
52
+ },
53
+ },
54
+ };