sprintify-ui 0.0.204 → 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 (30) hide show
  1. package/dist/sprintify-ui.es.js +19691 -14695
  2. package/dist/types/src/components/BaseAddressForm.vue.d.ts +24 -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.vue +27 -6
  16. package/src/components/BaseMediaGallery.vue +95 -0
  17. package/src/components/BaseMediaGalleryItem.vue +92 -0
  18. package/src/components/BaseMediaItem.vue +1 -1
  19. package/src/components/BaseMediaLibrary.stories.js +181 -19
  20. package/src/components/BaseMediaLibrary.vue +94 -102
  21. package/src/components/BaseMediaList.vue +70 -0
  22. package/src/components/BaseMediaListItem.vue +171 -0
  23. package/src/components/BaseMediaPictures.vue +66 -0
  24. package/src/components/BaseMediaPicturesItem.vue +93 -0
  25. package/src/components/BaseMediaPreview.vue +16 -4
  26. package/src/lang/en.json +2 -0
  27. package/src/lang/fr.json +2 -0
  28. package/src/types/Media.ts +1 -0
  29. package/src/types/UploadedFile.ts +1 -0
  30. package/src/types/index.ts +2 -4
@@ -52,56 +52,64 @@
52
52
  </template>
53
53
  </BaseFileUploader>
54
54
 
55
- <div
56
- v-if="filteredCurrentMedia.length + normalizedModelValue.to_add.length"
57
- class="mt-5"
58
- >
59
- <div class="-m-1 flex flex-wrap">
60
- <div
61
- v-for="(media, index) in filteredCurrentMedia"
62
- :key="media.id"
63
- class="min-w-[220px] flex-1 p-1"
64
- >
65
- <BaseMediaItem
66
- :media="media"
67
- :show-remove="!disabled"
68
- @remove="promptRemoveMedia(index)"
69
- >
70
- {{ media.file_name }}
71
- </BaseMediaItem>
72
- </div>
73
-
74
- <div
75
- v-for="(file, index) in normalizedModelValue.to_add"
76
- :key="file.id"
77
- class="min-w-[220px] flex-1 p-1"
78
- >
79
- <BaseMediaItem
80
- :media="file"
81
- :show-remove="!disabled"
82
- @remove="promptRemoveUploadedFile(index)"
83
- >
84
- {{ file.file_name }}
85
- </BaseMediaItem>
86
- </div>
87
- </div>
55
+ <div v-if="normalizedModelValue.length" class="mt-5">
56
+ <slot
57
+ :model-value="normalizedModelValue"
58
+ name="list"
59
+ :disabled="disabled"
60
+ :draggable="draggable"
61
+ :remove="promptRemove"
62
+ @update:model-value="sync"
63
+ >
64
+ <BaseMediaPictures
65
+ v-if="layout == 'images'"
66
+ v-bind="listProps"
67
+ :model-value="normalizedModelValue"
68
+ :disabled="disabled"
69
+ :draggable="draggable"
70
+ @update:model-value="sync"
71
+ @remove="promptRemove($event)"
72
+ ></BaseMediaPictures>
73
+
74
+ <BaseMediaList
75
+ v-else-if="layout == 'list'"
76
+ v-bind="listProps"
77
+ :model-value="normalizedModelValue"
78
+ :disabled="disabled"
79
+ :draggable="draggable"
80
+ @update:model-value="sync"
81
+ @remove="promptRemove($event)"
82
+ ></BaseMediaList>
83
+
84
+ <BaseMediaGallery
85
+ v-else-if="layout == 'gallery'"
86
+ v-bind="listProps"
87
+ :model-value="normalizedModelValue"
88
+ :disabled="disabled"
89
+ :draggable="draggable"
90
+ @update:model-value="sync"
91
+ @remove="promptRemove($event)"
92
+ ></BaseMediaGallery>
93
+ </slot>
88
94
  </div>
89
95
  </div>
90
96
  </template>
91
97
 
92
98
  <script lang="ts" setup>
93
99
  import { PropType } from 'vue';
94
- import { cloneDeep, isArray, isObject, capitalize } from 'lodash';
100
+ import { cloneDeep, isArray, capitalize, uniqueId } from 'lodash';
95
101
  import { UploadedFile } from '@/types/UploadedFile';
96
102
  import { Media } from '@/types/Media';
97
103
  import { BaseCropperConfig, MediaLibraryPayload } from '@/types';
98
104
  import { fileSizeFormat } from '@/utils';
99
105
  import { useDialogsStore } from '@/stores/dialogs';
100
106
  import { useNotificationsStore } from '@/stores/notifications';
101
- import BaseMediaItem from '@/components/BaseMediaItem.vue';
102
107
  import BaseFileUploader from './BaseFileUploader.vue';
103
108
  import { useField } from '@/composables/field';
104
109
  import { BaseIcon } from '.';
110
+ import BaseMediaList from './BaseMediaList.vue';
111
+ import BaseMediaPictures from './BaseMediaPictures.vue';
112
+ import BaseMediaGallery from './BaseMediaGallery.vue';
105
113
 
106
114
  const i18n = useI18n();
107
115
 
@@ -111,7 +119,7 @@ const notifications = useNotificationsStore();
111
119
  const props = defineProps({
112
120
  modelValue: {
113
121
  default: undefined,
114
- type: Object as PropType<MediaLibraryPayload | null | undefined>,
122
+ type: Object as PropType<MediaLibraryPayload>,
115
123
  },
116
124
  name: {
117
125
  default: undefined,
@@ -163,6 +171,18 @@ const props = defineProps({
163
171
  default: undefined,
164
172
  type: Object as PropType<BaseCropperConfig | boolean | null>,
165
173
  },
174
+ draggable: {
175
+ default: false,
176
+ type: Boolean,
177
+ },
178
+ listProps: {
179
+ default: undefined,
180
+ type: Object as PropType<Record<string, unknown>>,
181
+ },
182
+ layout: {
183
+ default: 'gallery',
184
+ type: String as PropType<'list' | 'images' | 'gallery'>,
185
+ },
166
186
  });
167
187
 
168
188
  const emit = defineEmits([
@@ -173,12 +193,6 @@ const emit = defineEmits([
173
193
  'upload:end',
174
194
  ]);
175
195
 
176
- const filteredCurrentMedia = computed(() => {
177
- return props.currentMedia.filter((media) => {
178
- return !normalizedModelValue.value.to_remove.includes(media.id);
179
- });
180
- });
181
-
182
196
  const { emitUpdate, enableForm, disableForm } = useField({
183
197
  name: computed(() => props.name),
184
198
  required: computed(() => false),
@@ -187,6 +201,24 @@ const { emitUpdate, enableForm, disableForm } = useField({
187
201
  errorType: 'alert',
188
202
  });
189
203
 
204
+ // Model value normalization
205
+
206
+ const normalizedModelValue = computed(() => {
207
+ if (!isArray(props.modelValue)) {
208
+ return [];
209
+ }
210
+
211
+ return props.modelValue;
212
+ });
213
+
214
+ sync(normalizedModelValue.value);
215
+
216
+ const numberOfFiles = computed((): number => {
217
+ return normalizedModelValue.value.length;
218
+ });
219
+
220
+ // Validations
221
+
190
222
  const normalizedMax = computed(() => {
191
223
  if (props.max == null) {
192
224
  return 100;
@@ -195,31 +227,19 @@ const normalizedMax = computed(() => {
195
227
  return props.max;
196
228
  });
197
229
 
198
- const normalizedModelValue = computed(() => {
199
- if (
200
- props.modelValue &&
201
- isObject(props.modelValue) &&
202
- isArray(props.modelValue.to_add) &&
203
- isArray(props.modelValue.to_remove)
204
- ) {
205
- return props.modelValue;
206
- }
207
-
208
- return {
209
- to_add: [],
210
- to_remove: [],
211
- };
230
+ const maxFileText = computed(() => {
231
+ return i18n.t('sui.you_can_upload_up_to_n_files', {
232
+ count: normalizedMax.value,
233
+ });
212
234
  });
213
235
 
214
- const numberOfFiles = computed((): number => {
215
- return (
216
- props.currentMedia.length +
217
- (props.modelValue?.to_add.length ?? 0) -
218
- (props.modelValue?.to_remove.length ?? 0)
236
+ const maxFileSize = computed(() => {
237
+ return capitalize(
238
+ i18n.t('sui.up_to_x', { x: fileSizeFormat(props.maxSize) })
219
239
  );
220
240
  });
221
241
 
222
- sync(normalizedModelValue.value);
242
+ // Upload
223
243
 
224
244
  function onUploadSuccess(file: UploadedFile) {
225
245
  if (file == null) {
@@ -241,78 +261,50 @@ function onUploadSuccess(file: UploadedFile) {
241
261
  return;
242
262
  }
243
263
 
244
- const modelValue = cloneDeep(normalizedModelValue.value);
264
+ let modelValue = cloneDeep(normalizedModelValue.value);
245
265
 
246
266
  if (normalizedMax.value == 1) {
247
267
  // Remove everything...
248
- modelValue.to_remove.push(...props.currentMedia.map((m) => m.id));
249
- modelValue.to_add = [];
268
+ modelValue = [];
250
269
  }
251
270
 
252
- modelValue.to_add.push(file);
271
+ file.id = 'new' + uniqueId();
272
+
273
+ modelValue.push(file);
253
274
 
254
275
  sync(modelValue);
255
276
 
256
277
  emit('upload:success', file);
257
278
  }
258
279
 
259
- function promptRemoveUploadedFile(index: number, length = 1) {
260
- dialogs.push({
261
- title: i18n.t('sui.remove_file'),
262
- message: i18n.t('sui.remove_file_description'),
263
- color: 'warning',
264
- onConfirm() {
265
- removeUploadedFile(index, length);
266
- },
267
- });
268
- }
280
+ // Remove
269
281
 
270
- function promptRemoveMedia(index: number) {
282
+ function promptRemove(index: number, length = 1) {
271
283
  dialogs.push({
272
284
  title: i18n.t('sui.remove_file'),
273
285
  message: i18n.t('sui.remove_file_description'),
274
286
  color: 'warning',
275
287
  onConfirm() {
276
- removeMedia(index);
288
+ removeByIndex(index, length);
277
289
  },
278
290
  });
279
291
  }
280
292
 
281
- function removeUploadedFile(index: number, length = 1) {
293
+ function removeByIndex(index: number, length = 1) {
282
294
  const modelValue = cloneDeep(normalizedModelValue.value);
283
295
 
284
- modelValue?.to_add.splice(index, length);
296
+ modelValue.splice(index, length);
285
297
 
286
298
  sync(modelValue);
287
299
  }
288
300
 
289
- function removeMedia(index: number) {
290
- const media = props.currentMedia[index];
291
-
292
- if (media) {
293
- const modelValue = cloneDeep(normalizedModelValue.value);
294
-
295
- modelValue.to_remove.push(media.id);
296
-
297
- sync(modelValue);
298
- }
299
- }
301
+ // Sync
300
302
 
301
303
  function sync(modelValue: MediaLibraryPayload) {
302
304
  emitUpdate(modelValue);
303
305
  }
304
306
 
305
- const maxFileText = computed(() => {
306
- return i18n.t('sui.you_can_upload_up_to_n_files', {
307
- count: normalizedMax.value,
308
- });
309
- });
310
-
311
- const maxFileSize = computed(() => {
312
- return capitalize(
313
- i18n.t('sui.up_to_x', { x: fileSizeFormat(props.maxSize) })
314
- );
315
- });
307
+ // Events
316
308
 
317
309
  function onUploadStart(event: any) {
318
310
  emit('upload:start', event);
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <vuedraggable
3
+ :model-value="modelValue"
4
+ group="media"
5
+ item-key="id"
6
+ tag="div"
7
+ class="border-t border-slate-200"
8
+ handle=".handle"
9
+ :disabled="disabled"
10
+ @update:model-value="onDragUpdate"
11
+ >
12
+ <template #item="{ element, index }">
13
+ <div class="border-b border-slate-200">
14
+ <BaseMediaListItem
15
+ :media="element"
16
+ :show-remove="showRemove"
17
+ :draggable="draggable"
18
+ :disabled="disabled"
19
+ @update="onItemUpdate(index, $event)"
20
+ @remove="$emit('remove', index)"
21
+ @save:name="$emit('save:name', index, $event)"
22
+ ></BaseMediaListItem>
23
+ </div>
24
+ </template>
25
+ </vuedraggable>
26
+ </template>
27
+
28
+ <script lang="ts" setup>
29
+ import { Media } from '@/types/Media';
30
+ import { UploadedFile } from '@/types/UploadedFile';
31
+ import { cloneDeep } from 'lodash';
32
+ import { PropType } from 'vue';
33
+ import vuedraggable from 'vuedraggable';
34
+ import BaseMediaListItem from './BaseMediaListItem.vue';
35
+
36
+ const props = defineProps({
37
+ modelValue: {
38
+ required: true,
39
+ type: Object as PropType<(Media | UploadedFile)[]>,
40
+ },
41
+ showRemove: {
42
+ default: true,
43
+ type: Boolean,
44
+ },
45
+ disabled: {
46
+ default: false,
47
+ type: Boolean,
48
+ },
49
+ draggable: {
50
+ default: false,
51
+ type: Boolean,
52
+ },
53
+ });
54
+
55
+ const emit = defineEmits(['update:modelValue', 'remove', 'save:name']);
56
+
57
+ function onItemUpdate(index: number, media: Media | UploadedFile) {
58
+ const modelValue = cloneDeep(props.modelValue);
59
+ modelValue[index] = media;
60
+ emit('update:modelValue', modelValue);
61
+ }
62
+
63
+ function onDragUpdate(value: (Media | UploadedFile)[]) {
64
+ if (props.disabled) {
65
+ return;
66
+ }
67
+
68
+ emit('update:modelValue', value);
69
+ }
70
+ </script>
@@ -0,0 +1,171 @@
1
+ <template>
2
+ <li
3
+ class="group flex h-10 items-center justify-between"
4
+ :class="[draggable ? 'pr-2' : 'px-2']"
5
+ >
6
+ <div v-if="draggable && !disabled" class="handle shrink-0 cursor-move px-1">
7
+ <BaseIcon icon="mdi:drag" class="h-5 w-5 text-slate-400"></BaseIcon>
8
+ </div>
9
+ <div class="mr-2 shrink-0">
10
+ <BaseIcon
11
+ icon="heroicons-solid:paper-clip"
12
+ class="h-5 w-5 shrink-0 text-slate-400"
13
+ ></BaseIcon>
14
+ </div>
15
+ <div class="flex grow items-center gap-3 overflow-hidden">
16
+ <div
17
+ v-if="viewMode == 'show'"
18
+ class="flex grow items-center overflow-hidden"
19
+ >
20
+ <button
21
+ type="button"
22
+ :disabled="disabled"
23
+ class="flex h-10 items-center overflow-hidden"
24
+ @click="editName()"
25
+ >
26
+ <span class="mr-2 truncate text-sm">{{ fileName }}</span>
27
+ <div
28
+ v-if="!disabled"
29
+ class="text-center opacity-0 group-hover:opacity-100"
30
+ >
31
+ <BaseIcon
32
+ icon="heroicons-solid:pencil"
33
+ class="h-5 w-5 shrink-0 text-slate-400"
34
+ ></BaseIcon>
35
+ </div>
36
+ </button>
37
+ </div>
38
+
39
+ <div v-else class="flex grow items-center py-1">
40
+ <input
41
+ ref="inputRef"
42
+ class="h-8 w-full max-w-md rounded border border-slate-300 px-2 py-0 focus:ring-0"
43
+ type="text"
44
+ :value="name"
45
+ autofocus
46
+ @input="onNameChange"
47
+ @keydown.enter.prevent="saveName"
48
+ @keydown.escape.prevent="viewMode = 'show'"
49
+ />
50
+
51
+ <div class="flex shrink-0 items-center">
52
+ <button
53
+ type="button"
54
+ class="h-10 shrink-0 pr-2 pl-3 text-sm text-blue-600"
55
+ @click="saveName"
56
+ >
57
+ {{ $t('sui.save') }}
58
+ </button>
59
+ </div>
60
+ </div>
61
+
62
+ <div class="flex shrink-0 gap-2">
63
+ <button
64
+ v-if="showRemove"
65
+ type="button"
66
+ :disabled="disabled"
67
+ class="shrink-0 text-sm disabled:opacity-50"
68
+ @click="$emit('remove')"
69
+ >
70
+ <span class="hidden sm:inline">
71
+ {{ $t('sui.delete') }}
72
+ </span>
73
+ <BaseIcon
74
+ icon="heroicons-solid:x"
75
+ class="h-5 w-5 shrink-0 text-slate-400 sm:hidden"
76
+ />
77
+ </button>
78
+ <a
79
+ v-if="url"
80
+ :href="url"
81
+ class="shrink-0 text-sm text-blue-600"
82
+ target="_blank"
83
+ >
84
+ <span class="hidden sm:inline">
85
+ {{ $t('sui.download') }}
86
+ </span>
87
+ <BaseIcon
88
+ icon="heroicons-solid:download"
89
+ class="h-5 w-5 shrink-0 text-slate-400 sm:hidden"
90
+ />
91
+ </a>
92
+ </div>
93
+ </div>
94
+ </li>
95
+ </template>
96
+
97
+ <script lang="ts" setup>
98
+ import { Media } from '@/types/Media';
99
+ import { UploadedFile } from '@/types/UploadedFile';
100
+ import { PropType } from 'vue';
101
+ import { Icon as BaseIcon } from '@iconify/vue';
102
+ import { cloneDeep } from 'lodash';
103
+
104
+ const emit = defineEmits(['remove', 'update', 'save:name']);
105
+
106
+ const props = defineProps({
107
+ media: {
108
+ required: true,
109
+ type: Object as PropType<Media | UploadedFile>,
110
+ },
111
+ disabled: {
112
+ default: false,
113
+ type: Boolean,
114
+ },
115
+ showRemove: {
116
+ default: true,
117
+ type: Boolean,
118
+ },
119
+ draggable: {
120
+ default: false,
121
+ type: Boolean,
122
+ },
123
+ });
124
+
125
+ const viewMode = ref<'show' | 'edit'>('show');
126
+
127
+ const name = computed(() => {
128
+ return props.media.name;
129
+ });
130
+
131
+ const fileName = computed(() => {
132
+ return props.media.file_name;
133
+ });
134
+
135
+ const url = computed(() => {
136
+ if ('url' in props.media) {
137
+ return props.media.url;
138
+ }
139
+
140
+ return null;
141
+ });
142
+
143
+ const inputRef = ref<HTMLInputElement | null>();
144
+
145
+ function editName() {
146
+ if (props.disabled) {
147
+ return;
148
+ }
149
+
150
+ viewMode.value = 'edit';
151
+
152
+ setTimeout(() => {
153
+ inputRef.value?.focus();
154
+ inputRef.value?.select();
155
+ }, 100);
156
+ }
157
+
158
+ function onNameChange(e: Event) {
159
+ const target = e.target as HTMLInputElement;
160
+ const media = cloneDeep(props.media);
161
+ media.name = (target.value ?? '') as string;
162
+ const ext = media.file_name.split('.').pop();
163
+ media.file_name = media.name + '.' + ext;
164
+ emit('update', media);
165
+ }
166
+
167
+ function saveName() {
168
+ viewMode.value = 'show';
169
+ emit('save:name');
170
+ }
171
+ </script>
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <vuedraggable
3
+ :model-value="modelValue"
4
+ group="media"
5
+ item-key="id"
6
+ tag="div"
7
+ class="flex flex-wrap"
8
+ handle=".handle"
9
+ :disabled="disabled"
10
+ @update:model-value="onDragUpdate"
11
+ >
12
+ <template #item="{ element, index }">
13
+ <div>
14
+ <BaseMediaPicturesItem
15
+ :media="element"
16
+ :show-remove="showRemove"
17
+ :draggable="draggable"
18
+ :size="size"
19
+ :disabled="disabled"
20
+ @remove="$emit('remove', index)"
21
+ ></BaseMediaPicturesItem>
22
+ </div>
23
+ </template>
24
+ </vuedraggable>
25
+ </template>
26
+
27
+ <script lang="ts" setup>
28
+ import { Media } from '@/types/Media';
29
+ import { UploadedFile } from '@/types/UploadedFile';
30
+ import { PropType } from 'vue';
31
+ import vuedraggable from 'vuedraggable';
32
+ import BaseMediaPicturesItem from './BaseMediaPicturesItem.vue';
33
+
34
+ const props = defineProps({
35
+ modelValue: {
36
+ required: true,
37
+ type: Object as PropType<(Media | UploadedFile)[]>,
38
+ },
39
+ showRemove: {
40
+ default: true,
41
+ type: Boolean,
42
+ },
43
+ disabled: {
44
+ default: false,
45
+ type: Boolean,
46
+ },
47
+ draggable: {
48
+ default: false,
49
+ type: Boolean,
50
+ },
51
+ size: {
52
+ default: 140,
53
+ type: Number,
54
+ },
55
+ });
56
+
57
+ const emit = defineEmits(['update:modelValue', 'remove']);
58
+
59
+ function onDragUpdate(value: (Media | UploadedFile)[]) {
60
+ if (props.disabled) {
61
+ return;
62
+ }
63
+
64
+ emit('update:modelValue', value);
65
+ }
66
+ </script>
@@ -0,0 +1,93 @@
1
+ <template>
2
+ <div
3
+ class="relative mr-4 mb-4 flex flex-col items-center rounded-lg bg-slate-200 shadow-md ring-1 ring-black ring-opacity-10"
4
+ :class="[draggable && !disabled ? 'handle cursor-move' : 'cursor-default']"
5
+ :style="{
6
+ width: size + 'px',
7
+ height: size + 'px',
8
+ marginRight: spacing,
9
+ marginBottom: spacing,
10
+ }"
11
+ >
12
+ <img
13
+ :src="src"
14
+ :style="{
15
+ width: size + 'px',
16
+ height: size + 'px',
17
+ }"
18
+ class="overflow-hidden rounded-lg object-cover"
19
+ />
20
+ <div class="absolute -top-2 -right-2 flex gap-1">
21
+ <a
22
+ v-if="url"
23
+ :href="url"
24
+ target="_blank"
25
+ class="btn btn-white rounded-full p-1 shadow-sm ring-1 ring-black ring-opacity-10"
26
+ >
27
+ <BaseIcon class="h-4 w-4" icon="mdi:download"></BaseIcon>
28
+ </a>
29
+ <button
30
+ v-if="showRemove"
31
+ :disabled="disabled"
32
+ class="btn btn-white rounded-full p-1 shadow-sm ring-1 ring-black ring-opacity-10 disabled:bg-white disabled:opacity-70"
33
+ @click="$emit('remove')"
34
+ >
35
+ <BaseIcon class="h-4 w-4" icon="mdi:close"></BaseIcon>
36
+ </button>
37
+ </div>
38
+ </div>
39
+ </template>
40
+
41
+ <script lang="ts" setup>
42
+ import { Media } from '@/types/Media';
43
+ import { UploadedFile } from '@/types/UploadedFile';
44
+ import { PropType } from 'vue';
45
+ import { Icon as BaseIcon } from '@iconify/vue';
46
+
47
+ defineEmits(['remove']);
48
+
49
+ const props = defineProps({
50
+ media: {
51
+ required: true,
52
+ type: Object as PropType<Media | UploadedFile>,
53
+ },
54
+ showRemove: {
55
+ default: true,
56
+ type: Boolean,
57
+ },
58
+ draggable: {
59
+ default: false,
60
+ type: Boolean,
61
+ },
62
+ size: {
63
+ default: 140,
64
+ type: Number,
65
+ },
66
+ disabled: {
67
+ default: false,
68
+ type: Boolean,
69
+ },
70
+ });
71
+
72
+ const src = computed(() => {
73
+ if ('url' in props.media) {
74
+ return props.media.url;
75
+ }
76
+
77
+ return props.media.data_url;
78
+ });
79
+
80
+ const url = computed(() => {
81
+ if ('url' in props.media) {
82
+ return props.media.url;
83
+ }
84
+
85
+ return null;
86
+ });
87
+
88
+ const spacing = computed(() => {
89
+ const MAX_SPACING = 18;
90
+ const value = Math.min(MAX_SPACING, 0.15 * props.size);
91
+ return value + 'px';
92
+ });
93
+ </script>