sprintify-ui 0.0.203 → 0.1.0

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 (31) hide show
  1. package/dist/sprintify-ui.es.js +19600 -14587
  2. package/dist/types/src/components/BaseAddressForm.vue.d.ts +29 -3
  3. package/dist/types/src/components/BaseMediaGallery.vue.d.ts +64 -0
  4. package/dist/types/src/components/BaseMediaGalleryItem.vue.d.ts +45 -0
  5. package/dist/types/src/components/BaseMediaLibrary.vue.d.ts +37 -3
  6. package/dist/types/src/components/BaseMediaList.vue.d.ts +47 -0
  7. package/dist/types/src/components/BaseMediaListItem.vue.d.ts +47 -0
  8. package/dist/types/src/components/BaseMediaPictures.vue.d.ts +55 -0
  9. package/dist/types/src/components/BaseMediaPicturesItem.vue.d.ts +54 -0
  10. package/dist/types/src/index.d.ts +8 -0
  11. package/dist/types/src/types/Media.d.ts +1 -0
  12. package/dist/types/src/types/UploadedFile.d.ts +1 -0
  13. package/dist/types/src/types/index.d.ts +2 -4
  14. package/package.json +3 -2
  15. package/src/components/BaseAddressForm.stories.js +31 -0
  16. package/src/components/BaseAddressForm.vue +56 -9
  17. package/src/components/BaseMediaGallery.vue +95 -0
  18. package/src/components/BaseMediaGalleryItem.vue +92 -0
  19. package/src/components/BaseMediaItem.vue +1 -1
  20. package/src/components/BaseMediaLibrary.stories.js +181 -19
  21. package/src/components/BaseMediaLibrary.vue +94 -102
  22. package/src/components/BaseMediaList.vue +70 -0
  23. package/src/components/BaseMediaListItem.vue +171 -0
  24. package/src/components/BaseMediaPictures.vue +66 -0
  25. package/src/components/BaseMediaPicturesItem.vue +93 -0
  26. package/src/components/BaseMediaPreview.vue +16 -4
  27. package/src/lang/en.json +2 -0
  28. package/src/lang/fr.json +2 -0
  29. package/src/types/Media.ts +1 -0
  30. package/src/types/UploadedFile.ts +1 -0
  31. package/src/types/index.ts +2 -4
@@ -65,6 +65,7 @@
65
65
  class="w-full"
66
66
  :options="countries"
67
67
  label-key="name"
68
+ :disabled="props.restrictCountry"
68
69
  value-key="id"
69
70
  @update:model-value="update('country', $event)"
70
71
  >
@@ -100,7 +101,16 @@ import BaseInput from './BaseInput.vue';
100
101
  import BaseSelect from './BaseSelect.vue';
101
102
  import { config } from '..';
102
103
 
103
- type Address = Record<string, string | number | null | undefined>;
104
+ const DEFAULT_COUNTRY = 'ca';
105
+
106
+ type Address = {
107
+ address_1?: string | null;
108
+ address_2?: string | null;
109
+ postal_code?: string | null;
110
+ city?: string | null;
111
+ region?: string | null;
112
+ country?: string | null;
113
+ };
104
114
 
105
115
  const props = withDefaults(
106
116
  defineProps<{
@@ -108,6 +118,7 @@ const props = withDefaults(
108
118
  prefix: string | null;
109
119
  countries?: Country[];
110
120
  regions?: Region[];
121
+ restrictCountry?: boolean;
111
122
  }>(),
112
123
  {
113
124
  modelValue() {
@@ -120,13 +131,14 @@ const props = withDefaults(
120
131
  regions() {
121
132
  return [];
122
133
  },
134
+ restrictCountry: false,
123
135
  }
124
136
  );
125
137
 
126
138
  const emit = defineEmits(['update:model-value']);
127
139
 
128
140
  const normalizedModelValue = computed((): Address => {
129
- const form = cloneDeep(props.modelValue ?? {});
141
+ const form = cloneDeep(props.modelValue ?? {}) as Address;
130
142
  form.address_1 = form.address_1 ?? '';
131
143
  form.address_2 = form.address_2 ?? '';
132
144
  form.city = form.city ?? '';
@@ -136,6 +148,16 @@ const normalizedModelValue = computed((): Address => {
136
148
  return form;
137
149
  });
138
150
 
151
+ const normalizedCountry = computed((): string | null => {
152
+ if (props.restrictCountry) {
153
+ if (normalizedModelValue.value.country) {
154
+ return normalizedModelValue.value.country;
155
+ }
156
+ return DEFAULT_COUNTRY;
157
+ }
158
+ return normalizedModelValue.value.country ?? null;
159
+ });
160
+
139
161
  const countries = computed((): Country[] => {
140
162
  if (props.countries && isArray(props.countries) && props.countries.length) {
141
163
  return props.countries;
@@ -163,7 +185,7 @@ const namePrefix = computed((): string => {
163
185
  return '';
164
186
  });
165
187
 
166
- function update(field: string, value: string) {
188
+ function update(field: keyof Address, value: string) {
167
189
  const newForm = cloneDeep(normalizedModelValue.value);
168
190
  newForm[field] = value;
169
191
  emit('update:model-value', newForm);
@@ -189,17 +211,42 @@ onMounted(() => {
189
211
  }
190
212
 
191
213
  autocomplete = new window.google.maps.places.Autocomplete(
192
- address1Input.value,
193
- {
194
- fields: ['address_components'],
195
- types: ['address'],
196
- componentRestrictions: { country: 'ca' },
197
- }
214
+ address1Input.value
198
215
  );
199
216
 
217
+ setAutocompleteOptions();
218
+
200
219
  autocomplete.addListener('place_changed', fillAddress);
201
220
  });
202
221
 
222
+ watch(
223
+ () => props.modelValue?.country + ' ' + props.restrictCountry,
224
+ () => {
225
+ setAutocompleteOptions();
226
+ }
227
+ );
228
+
229
+ function setAutocompleteOptions() {
230
+ if (!autocomplete) {
231
+ return;
232
+ }
233
+
234
+ const options = {
235
+ fields: ['address_components'],
236
+ types: ['address'],
237
+ } as any;
238
+
239
+ if (props.restrictCountry) {
240
+ if (normalizedCountry.value) {
241
+ options.componentRestrictions = {
242
+ country: normalizedCountry.value,
243
+ };
244
+ }
245
+ }
246
+
247
+ autocomplete.setOptions(options);
248
+ }
249
+
203
250
  function fillAddress() {
204
251
  if (!autocomplete) {
205
252
  return;
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <div ref="itemsRef">
3
+ <vuedraggable
4
+ :model-value="modelValue"
5
+ group="media"
6
+ item-key="id"
7
+ tag="div"
8
+ :disabled="disabled"
9
+ class="-m-1 flex flex-wrap"
10
+ handle=".handle"
11
+ @update:model-value="onDragUpdate"
12
+ >
13
+ <template #item="{ element, index }">
14
+ <div :style="{ width: itemWidth }" class="p-1">
15
+ <BaseMediaGalleryItem
16
+ :media="element"
17
+ :show-remove="!disabled"
18
+ :disabled="disabled"
19
+ :draggable="draggable"
20
+ @remove="$emit('remove', index)"
21
+ >
22
+ {{ element.file_name }}
23
+ </BaseMediaGalleryItem>
24
+ </div>
25
+ </template>
26
+ </vuedraggable>
27
+ </div>
28
+ </template>
29
+
30
+ <script lang="ts" setup>
31
+ import { Media } from '@/types/Media';
32
+ import { UploadedFile } from '@/types/UploadedFile';
33
+ import { useElementSize } from '@vueuse/core';
34
+ import { PropType } from 'vue';
35
+ import vuedraggable from 'vuedraggable';
36
+ import BaseMediaGalleryItem from './BaseMediaGalleryItem.vue';
37
+
38
+ const props = defineProps({
39
+ modelValue: {
40
+ required: true,
41
+ type: Object as PropType<(Media | UploadedFile)[]>,
42
+ },
43
+ showRemove: {
44
+ default: true,
45
+ type: Boolean,
46
+ },
47
+ draggable: {
48
+ default: false,
49
+ type: Boolean,
50
+ },
51
+ size: {
52
+ default: 140,
53
+ type: Number,
54
+ },
55
+ disabled: {
56
+ default: false,
57
+ type: Boolean,
58
+ },
59
+ itemMaxWidth: {
60
+ default: 220,
61
+ type: Number,
62
+ },
63
+ });
64
+
65
+ const emit = defineEmits(['update:modelValue', 'remove']);
66
+
67
+ // Item width
68
+
69
+ const itemsRef = ref<HTMLElement | null>(null);
70
+ const itemWidth = ref('220px');
71
+
72
+ const itemsRefSize = useElementSize(itemsRef);
73
+
74
+ watch(
75
+ () => itemsRefSize.width.value,
76
+ (width) => {
77
+ if (width <= 300) {
78
+ itemWidth.value = '100%';
79
+ return;
80
+ }
81
+
82
+ const numberOfItems = Math.floor(width / props.itemMaxWidth);
83
+
84
+ itemWidth.value = `${100 / numberOfItems}%`;
85
+ }
86
+ );
87
+
88
+ function onDragUpdate(value: (Media | UploadedFile)[]) {
89
+ if (props.disabled) {
90
+ return;
91
+ }
92
+
93
+ emit('update:modelValue', value);
94
+ }
95
+ </script>
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <div
3
+ class="relative overflow-hidden rounded bg-white shadow ring-1 ring-black ring-opacity-10"
4
+ >
5
+ <div
6
+ class="flex"
7
+ :class="{
8
+ 'pr-4': draggable,
9
+ }"
10
+ >
11
+ <div
12
+ v-if="draggable && !disabled"
13
+ class="handle flex shrink-0 cursor-move items-center justify-center border-r border-slate-300 bg-slate-200 px-1"
14
+ >
15
+ <BaseIcon icon="mdi:drag"></BaseIcon>
16
+ </div>
17
+ <div class="shrink-0">
18
+ <BaseMediaPreview class="h-12 w-12" :media="media" />
19
+ </div>
20
+ <component
21
+ :is="url ? 'a' : 'p'"
22
+ :href="url"
23
+ target="_blank"
24
+ class="flex grow items-center overflow-hidden px-3"
25
+ >
26
+ <div class="overflow-hidden text-left leading-tight">
27
+ <p class="mb-px grow truncate text-[13px] font-medium">
28
+ {{ name }}
29
+ </p>
30
+ <p class="shrink-0 text-[10px] text-slate-400">
31
+ {{ fileSize }}
32
+ </p>
33
+ </div>
34
+ </component>
35
+ </div>
36
+ <div v-if="showRemove" class="absolute top-px right-px">
37
+ <button
38
+ type="button"
39
+ class="rounded-full bg-white p-1 text-slate-500 hover:bg-slate-100"
40
+ @click="$emit('remove')"
41
+ >
42
+ <BaseIcon icon="heroicons:x-mark-20-solid" class="h-5 w-5"></BaseIcon>
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </template>
47
+
48
+ <script lang="ts" setup>
49
+ import { Media } from '@/types/Media';
50
+ import { UploadedFile } from '@/types/UploadedFile';
51
+ import { PropType } from 'vue';
52
+ import { fileSizeFormat } from '@/utils';
53
+ import BaseMediaPreview from './BaseMediaPreview.vue';
54
+ import { Icon as BaseIcon } from '@iconify/vue';
55
+
56
+ defineEmits(['remove']);
57
+
58
+ const props = defineProps({
59
+ media: {
60
+ required: true,
61
+ type: Object as PropType<Media | UploadedFile>,
62
+ },
63
+ showRemove: {
64
+ default: true,
65
+ type: Boolean,
66
+ },
67
+ draggable: {
68
+ default: false,
69
+ type: Boolean,
70
+ },
71
+ disabled: {
72
+ default: false,
73
+ type: Boolean,
74
+ },
75
+ });
76
+
77
+ const name = computed(() => {
78
+ return props.media.file_name;
79
+ });
80
+
81
+ const fileSize = computed(() => {
82
+ return fileSizeFormat(props.media.size);
83
+ });
84
+
85
+ const url = computed(() => {
86
+ if ('url' in props.media) {
87
+ return props.media.url;
88
+ }
89
+
90
+ return null;
91
+ });
92
+ </script>
@@ -10,7 +10,7 @@
10
10
  target="_blank"
11
11
  class="flex grow items-center overflow-hidden px-3"
12
12
  >
13
- <div class="text-left leading-tight">
13
+ <div class="overflow-hidden text-left leading-tight">
14
14
  <p class="mb-px grow truncate text-[13px] font-medium">
15
15
  {{ name }}
16
16
  </p>
@@ -1,15 +1,27 @@
1
1
  import BaseApp from './BaseApp.vue';
2
2
  import BaseMediaLibrary from './BaseMediaLibrary.vue';
3
+ import { BaseIcon } from '../components';
4
+ import ShowValue from '../../.storybook/components/ShowValue.vue';
3
5
  import { createFieldStory } from '@/../.storybook/utils';
4
6
 
5
7
  const mediaModel = {
6
8
  id: 'xxxxx',
9
+ name: 'picture0-1-2dfjjje-23refg-45t',
7
10
  file_name: 'picture0-1-2dfjjje-23refg-45t.jpg',
8
11
  mime_type: 'image/jpg',
9
12
  url: 'https://images.unsplash.com/photo-1670139018938-af8bf748a1bc?auto=format&fit=crop&w=1200&h=800&q=80',
10
13
  size: 430 * 1024,
11
14
  };
12
15
 
16
+ const mediaModel2 = {
17
+ id: 'yyyyy',
18
+ name: 'photo-1678729465418-ee8127e8c5cd',
19
+ file_name: 'photo-1678729465418-ee8127e8c5cd.jpg',
20
+ mime_type: 'image/jpg',
21
+ url: 'https://images.unsplash.com/photo-1678729465418-ee8127e8c5cd?auto=format&fit=crop&w=800&q=80',
22
+ size: 430 * 1024,
23
+ };
24
+
13
25
  export default {
14
26
  title: 'Form/BaseMediaLibrary',
15
27
  component: BaseMediaLibrary,
@@ -18,12 +30,24 @@ export default {
18
30
  min: 2,
19
31
  acceptedExtensions: ['jpg', 'jpeg', 'png', 'webp'],
20
32
  uploadUrl: 'https://faker.witify.io/api/todos/upload',
21
- currentMedia: [
33
+ },
34
+ };
35
+
36
+ const Template = (args) => ({
37
+ components: {
38
+ BaseApp,
39
+ BaseMediaLibrary,
40
+ ShowValue,
41
+ },
42
+ setup() {
43
+ const value = ref([
22
44
  mediaModel,
45
+ mediaModel2,
23
46
  {
24
47
  id: '1',
25
48
  url: '',
26
49
  mime_type: 'application/pdf',
50
+ name: 'document',
27
51
  file_name: 'document.pdf',
28
52
  size: 40012,
29
53
  },
@@ -31,6 +55,7 @@ export default {
31
55
  id: '2',
32
56
  url: '',
33
57
  mime_type: 'application/excel',
58
+ name: 'finance-2022',
34
59
  file_name: 'finance-2022.xlsx',
35
60
  size: 5461,
36
61
  },
@@ -38,6 +63,7 @@ export default {
38
63
  id: '3',
39
64
  url: '',
40
65
  mime_type: 'image/png',
66
+ name: '34345-1',
41
67
  file_name: '34345-1.png',
42
68
  size: 40012,
43
69
  },
@@ -45,30 +71,101 @@ export default {
45
71
  id: '4',
46
72
  url: '',
47
73
  mime_type: 'audio/mp3',
74
+ name: 'test',
48
75
  file_name: 'test.mp3',
49
76
  size: 792834,
50
77
  },
51
- ],
52
- },
53
- };
54
-
55
- const Template = (args) => ({
56
- components: {
57
- BaseApp,
58
- BaseMediaLibrary,
59
- },
60
- setup() {
61
- const value = ref(null);
78
+ ]);
62
79
  return { args, value };
63
80
  },
64
81
  template: `
65
82
  <BaseMediaLibrary v-model="value" v-bind="args" />
83
+ <ShowValue :value="value" />
66
84
  <BaseApp></BaseApp>
67
85
  `,
68
86
  });
69
87
 
70
- export const Demo = Template.bind({});
71
- Demo.args = {};
88
+ // Layout - Gallery
89
+
90
+ export const LayoutGallery = Template.bind({});
91
+ LayoutGallery.args = {
92
+ layout: 'gallery',
93
+ };
94
+
95
+ export const LayoutGalleryDisabled = Template.bind({});
96
+ LayoutGalleryDisabled.args = {
97
+ layout: 'gallery',
98
+ disabled: true,
99
+ };
100
+
101
+ export const LayoutGalleryDraggable = Template.bind({});
102
+ LayoutGalleryDraggable.args = {
103
+ layout: 'gallery',
104
+ draggable: true,
105
+ };
106
+
107
+ export const LayoutGalleryDraggableDisabled = Template.bind({});
108
+ LayoutGalleryDraggableDisabled.args = {
109
+ layout: 'gallery',
110
+ disabled: true,
111
+ draggable: true,
112
+ };
113
+
114
+ // Layout - List
115
+
116
+ export const LayoutList = Template.bind({});
117
+ LayoutList.args = {
118
+ layout: 'list',
119
+ };
120
+
121
+ export const LayoutListDisabled = Template.bind({});
122
+ LayoutListDisabled.args = {
123
+ layout: 'list',
124
+ disabled: true,
125
+ };
126
+
127
+ export const LayoutListDraggable = Template.bind({});
128
+ LayoutListDraggable.args = {
129
+ layout: 'list',
130
+ draggable: true,
131
+ };
132
+
133
+ export const LayoutListDraggableDisabled = Template.bind({});
134
+ LayoutListDraggableDisabled.args = {
135
+ layout: 'list',
136
+ disabled: true,
137
+ draggable: true,
138
+ };
139
+
140
+ // Layout - Images
141
+
142
+ export const LayoutImages = Template.bind({});
143
+ LayoutImages.args = {
144
+ pickerComponent: 'BaseFilePickerCrop',
145
+ layout: 'images',
146
+ };
147
+
148
+ export const LayoutImagesDisabled = Template.bind({});
149
+ LayoutImagesDisabled.args = {
150
+ pickerComponent: 'BaseFilePickerCrop',
151
+ layout: 'images',
152
+ disabled: true,
153
+ };
154
+
155
+ export const LayoutImagesDraggable = Template.bind({});
156
+ LayoutImagesDraggable.args = {
157
+ pickerComponent: 'BaseFilePickerCrop',
158
+ layout: 'images',
159
+ draggable: true,
160
+ };
161
+
162
+ export const LayoutImagesDraggableDisabled = Template.bind({});
163
+ LayoutImagesDraggableDisabled.args = {
164
+ pickerComponent: 'BaseFilePickerCrop',
165
+ layout: 'images',
166
+ disabled: true,
167
+ draggable: true,
168
+ };
72
169
 
73
170
  export const Crop = Template.bind({});
74
171
  Crop.args = {
@@ -89,12 +186,77 @@ MaxSize.args = {
89
186
  maxSize: 10 * 1024,
90
187
  };
91
188
 
92
- export const Disabled = Template.bind({});
93
- Disabled.args = {
94
- disabled: true,
95
- };
96
-
97
189
  export const Field = createFieldStory({
98
190
  component: BaseMediaLibrary,
99
191
  componentName: 'BaseMediaLibrary',
100
192
  });
193
+
194
+ // Slots
195
+
196
+ const SlotDefaultTemplate = (args) => ({
197
+ components: {
198
+ BaseApp,
199
+ BaseMediaLibrary,
200
+ ShowValue,
201
+ },
202
+ setup() {
203
+ const value = ref([mediaModel, mediaModel2]);
204
+ return { args, value };
205
+ },
206
+ template: `
207
+ <BaseMediaLibrary v-model="value" v-bind="args">
208
+ <template #default>
209
+ <div class="btn btn-lg w-full">
210
+ Upload!
211
+ </div>
212
+ </template>
213
+ </BaseMediaLibrary>
214
+
215
+ <ShowValue :value="value" />
216
+ <BaseApp></BaseApp>
217
+ `,
218
+ });
219
+
220
+ export const SlotDefault = SlotDefaultTemplate.bind({});
221
+ SlotDefault.args = {};
222
+
223
+ const SlotListTemplate = (args) => ({
224
+ components: {
225
+ BaseApp,
226
+ BaseMediaLibrary,
227
+ ShowValue,
228
+ },
229
+ setup() {
230
+ const value = ref([mediaModel, mediaModel2]);
231
+ return { args, value };
232
+ },
233
+ template: `
234
+ <BaseMediaLibrary v-model="value" v-bind="args">
235
+ <template #list="{ modelValue, remove }">
236
+ <div v-for="(item, i) in modelValue" :key="item.id" class="mb-3">
237
+ <div class="flex items-center">
238
+ <div class="w-12 h-12 mr-4">
239
+ <img :src="item.url" class="w-full h-full object-cover" />
240
+ </div>
241
+ <div class="flex-1">
242
+ <div class="text-sm font-semibold">{{ item.name }}</div>
243
+ <div class="text-xs text-gray-500">{{ item.file_name }}</div>
244
+ </div>
245
+ <div class="flex items-center">
246
+ <div class="text-xs text-gray-500 mr-2">{{ item.size }}</div>
247
+ <div class="text-xs text-gray-500 mr-2">{{ item.mime_type }}</div>
248
+ </div>
249
+ </div>
250
+
251
+ <button type="button" @click="remove(i)" class="text-xs text-red-500">Remove</button>
252
+ </div>
253
+ </template>
254
+ </BaseMediaLibrary>
255
+
256
+ <ShowValue :value="value" />
257
+ <BaseApp></BaseApp>
258
+ `,
259
+ });
260
+
261
+ export const SlotList = SlotListTemplate.bind({});
262
+ SlotList.args = {};