sprintify-ui 0.0.12 → 0.0.14

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 (56) hide show
  1. package/README.md +8 -7
  2. package/dist/sprintify-ui.es.js +4429 -3588
  3. package/dist/style.css +1 -1
  4. package/dist/tailwindcss/index.js +13 -4
  5. package/dist/types/src/components/BaseAutocomplete.vue.d.ts +8 -5
  6. package/dist/types/src/components/BaseAutocompleteFetch.vue.d.ts +8 -5
  7. package/dist/types/src/components/BaseBelongsTo.vue.d.ts +8 -5
  8. package/dist/types/src/components/BaseCharacterCounter.vue.d.ts +4 -4
  9. package/dist/types/src/components/BaseDatePicker.vue.d.ts +8 -5
  10. package/dist/types/src/components/BaseHasMany.vue.d.ts +277 -0
  11. package/dist/types/src/components/BaseInput.vue.d.ts +39 -1
  12. package/dist/types/src/components/{BaseMediaLibraryItem.vue.d.ts → BaseMediaItem.vue.d.ts} +26 -4
  13. package/dist/types/src/components/BaseMediaLibrary.vue.d.ts +23 -15
  14. package/dist/types/src/components/BaseMediaPreview.vue.d.ts +97 -0
  15. package/dist/types/src/components/BaseSelect.vue.d.ts +7 -7
  16. package/dist/types/src/components/BaseSideNavigationItem.vue.d.ts +20 -1
  17. package/dist/types/src/components/BaseTabItem.vue.d.ts +20 -1
  18. package/dist/types/src/components/BaseTagAutocomplete.vue.d.ts +25 -17
  19. package/dist/types/src/components/BaseTagAutocompleteFetch.vue.d.ts +37 -21
  20. package/dist/types/src/components/BaseTextarea.vue.d.ts +8 -5
  21. package/dist/types/src/components/index.d.ts +10 -4
  22. package/package.json +1 -1
  23. package/src/components/BaseAppDialogs.vue +2 -2
  24. package/src/components/BaseAppNotifications.vue +1 -1
  25. package/src/components/BaseAutocomplete.vue +18 -20
  26. package/src/components/BaseAutocompleteFetch.vue +2 -2
  27. package/src/components/BaseBelongsTo.vue +3 -2
  28. package/src/components/BaseClipboard.vue +1 -1
  29. package/src/components/BaseDatePicker.vue +2 -2
  30. package/src/components/BaseHasMany.vue +92 -0
  31. package/src/components/BaseInput.stories.js +20 -1
  32. package/src/components/BaseInput.vue +42 -14
  33. package/src/components/BaseMediaItem.stories.js +41 -0
  34. package/src/components/BaseMediaItem.vue +71 -0
  35. package/src/components/BaseMediaLibrary.stories.js +80 -0
  36. package/src/components/BaseMediaLibrary.vue +67 -68
  37. package/src/components/BaseMediaPreview.stories.js +72 -0
  38. package/src/components/BaseMediaPreview.vue +90 -0
  39. package/src/components/BaseMenu.vue +1 -1
  40. package/src/components/BaseSelect.vue +1 -1
  41. package/src/components/BaseSideNavigationItem.vue +11 -3
  42. package/src/components/BaseSystemAlert.vue +1 -1
  43. package/src/components/BaseTabItem.vue +13 -3
  44. package/src/components/BaseTable.vue +2 -2
  45. package/src/components/BaseTagAutocomplete.stories.js +129 -0
  46. package/src/components/BaseTagAutocomplete.vue +155 -57
  47. package/src/components/BaseTagAutocompleteFetch.stories.js +130 -0
  48. package/src/components/BaseTagAutocompleteFetch.vue +36 -25
  49. package/src/components/BaseTextarea.vue +2 -2
  50. package/src/components/HasMany.stories.js +135 -0
  51. package/src/components/index.ts +18 -6
  52. package/src/lang/en.json +1 -1
  53. package/src/lang/fr.json +1 -1
  54. package/dist/types/src/components/BasePaginationSimple.vue.d.ts +0 -25
  55. package/src/components/BaseMediaLibraryItem.vue +0 -92
  56. package/src/components/BasePaginationSimple.vue +0 -60
@@ -1,23 +1,43 @@
1
1
  <template>
2
- <input
3
- ref="input"
4
- :value="modelValue"
5
- :type="type"
6
- :name="name"
7
- :step="step"
8
- :disabled="disabled"
9
- :placeholder="placeholder"
10
- :required="required"
11
- class="rounded border-slate-300 disabled:cursor-not-allowed disabled:text-slate-300"
12
- :autocomplete="autocomplete ? 'on' : 'off'"
13
- @keydown.enter="onEnter"
14
- @input="$emit('update:modelValue', transformInputValue($event))"
15
- />
2
+ <div class="inline-flex rounded border border-slate-300">
3
+ <div
4
+ v-if="icon && iconPosition == 'left'"
5
+ class="flex shrink-0 items-center justify-center rounded-l border-r border-slate-300 bg-slate-100 px-3 text-slate-600"
6
+ >
7
+ <BaseIcon icon="heroicons:phone-20-solid" />
8
+ </div>
9
+ <input
10
+ ref="input"
11
+ :value="modelValue"
12
+ :type="type"
13
+ :name="name"
14
+ :step="step"
15
+ :disabled="disabled"
16
+ :placeholder="placeholder"
17
+ :required="required"
18
+ class="border-none bg-transparent outline-none focus:z-[1] focus:ring-2 focus:ring-primary-600 focus:ring-offset-1 disabled:cursor-not-allowed disabled:text-slate-300"
19
+ :class="{
20
+ 'rounded-r': icon && iconPosition == 'left',
21
+ 'rounded-l': icon && iconPosition == 'right',
22
+ rounded: !icon,
23
+ }"
24
+ :autocomplete="autocomplete ? 'on' : 'off'"
25
+ @keydown.enter="onEnter"
26
+ @input="$emit('update:modelValue', transformInputValue($event))"
27
+ />
28
+ <div
29
+ v-if="icon && iconPosition == 'right'"
30
+ class="flex shrink-0 items-center justify-center rounded-r border-l border-slate-300 bg-slate-100 px-3 text-slate-600"
31
+ >
32
+ <BaseIcon icon="heroicons:phone-20-solid" />
33
+ </div>
34
+ </div>
16
35
  </template>
17
36
 
18
37
  <script lang="ts" setup>
19
38
  import { get, isNumber, isString, trim } from 'lodash';
20
39
  import { PropType } from 'vue';
40
+ import { BaseIcon } from './index';
21
41
 
22
42
  const props = defineProps({
23
43
  modelValue: {
@@ -59,6 +79,14 @@ const props = defineProps({
59
79
  default: false,
60
80
  type: Boolean,
61
81
  },
82
+ icon: {
83
+ default: null,
84
+ type: String,
85
+ },
86
+ iconPosition: {
87
+ default: 'left',
88
+ type: String as PropType<'left' | 'right'>,
89
+ },
62
90
  });
63
91
 
64
92
  defineEmits(['update:modelValue']);
@@ -0,0 +1,41 @@
1
+ import BaseMediaItem from './BaseMediaItem.vue';
2
+
3
+ export default {
4
+ title: 'Components/BaseMediaItem',
5
+ component: BaseMediaItem,
6
+ args: {},
7
+ };
8
+
9
+ const Template = (args) => ({
10
+ components: {
11
+ BaseMediaItem,
12
+ },
13
+ setup() {
14
+ return { args };
15
+ },
16
+ template: `
17
+ <BaseMediaItem v-bind="args" />
18
+ `,
19
+ });
20
+
21
+ export const Demo = Template.bind({});
22
+ Demo.args = {
23
+ media: {
24
+ id: 'xxxxxxxxxxx',
25
+ file_name: 'picture.jpg',
26
+ url: 'https://images.unsplash.com/photo-1670139018938-af8bf748a1bc?auto=format&fit=crop&w=800&h=800&q=80',
27
+ mime_type: 'image',
28
+ size: 430 * 1024,
29
+ },
30
+ };
31
+
32
+ export const PDF = Template.bind({});
33
+ PDF.args = {
34
+ media: {
35
+ id: 'xxxxxxxxxxx',
36
+ file_name: 'document.pdf',
37
+ url: 'https://images.unsplash.com/photo-1670139018938-af8bf748a1bc?auto=format&fit=crop&w=800&h=800&q=80',
38
+ mime_type: 'application/pdf',
39
+ size: 430 * 1024,
40
+ },
41
+ };
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <div class="rounded border border-slate-300 bg-white shadow">
3
+ <div class="relative flex">
4
+ <div class="shrink-0">
5
+ <BaseMediaPreview class="h-14 w-14 rounded-l" :media="media" />
6
+ </div>
7
+ <component
8
+ :is="url ? 'a' : 'p'"
9
+ :href="url"
10
+ target="_blank"
11
+ class="grow overflow-hidden px-3 pt-3"
12
+ >
13
+ <div class="text-left leading-tight">
14
+ <p class="grow truncate text-sm font-medium">
15
+ {{ name }}
16
+ </p>
17
+ <p class="shrink-0 text-[10px] font-medium text-slate-400">
18
+ {{ size }}
19
+ </p>
20
+ </div>
21
+ </component>
22
+ <div v-if="showRemove" class="shrink-0 p-2">
23
+ <button
24
+ type="button"
25
+ class="rounded-full bg-slate-400 p-1 text-white hover:bg-slate-500"
26
+ @click="$emit('delete')"
27
+ >
28
+ <BaseIcon icon="heroicons:x-mark-20-solid" class="h-4 w-4"></BaseIcon>
29
+ </button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script lang="ts" setup>
36
+ import { Media } from '@/types/Media';
37
+ import { UploadedFile } from '@/types/UploadedFile';
38
+ import { PropType } from 'vue';
39
+ import { fileSizeFormat } from '@/utils';
40
+ import BaseMediaPreview from './BaseMediaPreview.vue';
41
+ import { BaseIcon } from '.';
42
+
43
+ defineEmits(['delete']);
44
+
45
+ const props = defineProps({
46
+ media: {
47
+ required: true,
48
+ type: Object as PropType<Media | UploadedFile>,
49
+ },
50
+ showRemove: {
51
+ default: true,
52
+ type: Boolean,
53
+ },
54
+ });
55
+
56
+ const name = computed(() => {
57
+ return props.media.file_name;
58
+ });
59
+
60
+ const size = computed(() => {
61
+ return fileSizeFormat(props.media.size);
62
+ });
63
+
64
+ const url = computed(() => {
65
+ if ('url' in props.media) {
66
+ return props.media.url;
67
+ }
68
+
69
+ return null;
70
+ });
71
+ </script>
@@ -0,0 +1,80 @@
1
+ import BaseApp from './BaseApp.vue';
2
+ import BaseMediaLibrary from './BaseMediaLibrary.vue';
3
+
4
+ const mediaModel = {
5
+ id: 'xxxxx',
6
+ file_name: 'picture0-1-2dfjjje-23refg-45t.jpg',
7
+ mime_type: 'image/jpg',
8
+ url: 'https://images.unsplash.com/photo-1670139018938-af8bf748a1bc?auto=format&fit=crop&w=1200&h=800&q=80',
9
+ size: 430 * 1024,
10
+ };
11
+
12
+ export default {
13
+ title: 'Form/BaseMediaLibrary',
14
+ component: BaseMediaLibrary,
15
+ args: {
16
+ name: 'media',
17
+ max: 2,
18
+ min: 2,
19
+ acceptedExtensions: ['jpg', 'png'],
20
+ maxSize: 500 * 1024,
21
+ currentMedia: [
22
+ mediaModel,
23
+ {
24
+ id: '1',
25
+ url: '',
26
+ mime_type: 'application/pdf',
27
+ file_name: 'document.pdf',
28
+ size: 40012,
29
+ },
30
+ {
31
+ id: '2',
32
+ url: '',
33
+ mime_type: 'application/excel',
34
+ file_name: 'finance-2022.xlsx',
35
+ size: 5461,
36
+ },
37
+ {
38
+ id: '3',
39
+ url: '',
40
+ mime_type: 'image/png',
41
+ file_name: '34345-1.png',
42
+ size: 40012,
43
+ },
44
+ {
45
+ id: '4',
46
+ url: '',
47
+ mime_type: 'audio/mp3',
48
+ file_name: 'test.mp3',
49
+ size: 792834,
50
+ },
51
+ ],
52
+ },
53
+ };
54
+
55
+ const Template = (args) => ({
56
+ components: {
57
+ BaseApp,
58
+ BaseMediaLibrary,
59
+ },
60
+ setup() {
61
+ return { args };
62
+ },
63
+ template: `
64
+ <BaseMediaLibrary v-bind="args" />
65
+ <BaseApp></BaseApp>
66
+ `,
67
+ });
68
+
69
+ export const Demo = Template.bind({});
70
+ Demo.args = {};
71
+
72
+ export const Disabled = Template.bind({});
73
+ Disabled.args = {
74
+ disabled: true,
75
+ };
76
+
77
+ export const Errors = Template.bind({});
78
+ Errors.args = {
79
+ errors: ['Whoops, you forgot the name of your mother!'],
80
+ };
@@ -1,29 +1,5 @@
1
1
  <template>
2
2
  <div>
3
- <div
4
- v-if="currentMediaInternal.length + normalizedModelValue.to_add.length"
5
- class="mb-5"
6
- >
7
- <div class="flex flex-wrap">
8
- <BaseMediaLibraryItem
9
- v-for="(media, index) in currentMediaInternal"
10
- :key="media.id"
11
- :media="media"
12
- @delete="promptRemoveMedia(index)"
13
- >
14
- {{ media.file_name }}
15
- </BaseMediaLibraryItem>
16
- <BaseMediaLibraryItem
17
- v-for="(file, index) in normalizedModelValue.to_add"
18
- :key="file.id"
19
- :media="file"
20
- @delete="promptRemoveUploadedFile(index)"
21
- >
22
- {{ file.file_name }}
23
- </BaseMediaLibraryItem>
24
- </div>
25
- </div>
26
-
27
3
  <BaseFileUploader
28
4
  :max-size="maxSize"
29
5
  :disabled="disabled"
@@ -44,34 +20,28 @@
44
20
  :max="max"
45
21
  >
46
22
  <div
47
- class="flex w-full items-start space-x-4 rounded-lg border-2 border-dashed border-slate-200 p-5 duration-200 hover:bg-slate-50 hover:enabled:bg-slate-100"
23
+ class="rounded border border-dashed p-6 duration-150"
48
24
  :class="[
49
- baseFileUploaderProps.dragging ? 'bg-slate-100' : 'bg-white',
50
- baseFileUploaderProps.disabled ? 'opacity-25' : '',
25
+ baseFileUploaderProps.dragging ? 'bg-blue-100' : 'bg-slate-100',
26
+ baseFileUploaderProps.disabled
27
+ ? 'cursor-not-allowed border-slate-300'
28
+ : 'border-slate-400 hover:bg-slate-50',
51
29
  ]"
52
30
  >
53
- <div class="rounded-full bg-slate-200 p-2">
31
+ <div :class="[baseFileUploaderProps.disabled ? 'opacity-30' : '']">
54
32
  <BaseIcon
55
33
  icon="heroicons:arrow-up-on-square"
56
- class="h-6 w-6 text-slate-500"
34
+ class="mx-auto mb-3 h-6 w-6 text-slate-500"
57
35
  />
58
- </div>
59
- <div class="text-left">
60
- <p class="mb-0 text-sm font-medium leading-tight">
61
- {{ $t('sui.drop_or_click_to_upload') }}
62
- </p>
63
-
64
- <div class="mt-1 text-sm leading-tight text-slate-500">
65
- <p v-if="max > 1">
66
- {{ $t('sui.you_can_upload_up_to_n_files', { count: max }) }}
67
- </p>
68
- <p>
69
- {{
70
- capitalize(
71
- $t('sui.up_to_x', { x: fileSizeFormat(maxSize) })
72
- )
73
- }}
36
+ <div class="text-center">
37
+ <p class="mb-0 text-sm font-medium leading-tight">
38
+ {{ $t('sui.drop_or_click_to_upload') }}
74
39
  </p>
40
+
41
+ <div class="mt-1 text-xs leading-tight text-slate-500">
42
+ <p v-if="maxFileText">{{ maxFileText }}</p>
43
+ <p>{{ maxFileSize }}</p>
44
+ </div>
75
45
  </div>
76
46
  </div>
77
47
  </div>
@@ -79,22 +49,54 @@
79
49
  </template>
80
50
  </BaseFileUploader>
81
51
 
82
- <p class="text-red-600">
52
+ <BaseAlert v-if="globalErrorMessage" class="mt-5" color="danger" bordered>
83
53
  {{ globalErrorMessage }}
84
- </p>
54
+ </BaseAlert>
55
+
56
+ <div
57
+ v-if="currentMediaInternal.length + normalizedModelValue.to_add.length"
58
+ class="mt-5"
59
+ >
60
+ <div class="grid gap-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
61
+ <div v-for="(media, index) in currentMediaInternal" :key="media.id">
62
+ <BaseMediaItem
63
+ :media="media"
64
+ :show-remove="!disabled"
65
+ @delete="promptRemoveMedia(index)"
66
+ >
67
+ {{ media.file_name }}
68
+ </BaseMediaItem>
69
+ </div>
70
+ <div
71
+ v-for="(file, index) in normalizedModelValue.to_add"
72
+ :key="file.id"
73
+ >
74
+ <BaseMediaItem
75
+ :media="file"
76
+ :show-remove="!disabled"
77
+ @delete="promptRemoveUploadedFile(index)"
78
+ >
79
+ {{ file.file_name }}
80
+ </BaseMediaItem>
81
+ </div>
82
+ </div>
83
+ </div>
85
84
  </div>
86
85
  </template>
87
86
 
88
87
  <script lang="ts" setup>
89
88
  import { UploadedFile } from '@/types/UploadedFile';
90
89
  import { Media } from '@/types/Media';
91
- import _, { cloneDeep, get } from 'lodash';
90
+ import { cloneDeep, isArray, isObject } from 'lodash';
92
91
  import { PropType } from 'vue';
93
92
  import { MediaLibraryPayload } from '@/types/types';
94
93
  import { useDialogsStore } from '../stores/dialogs';
95
94
  import { useNotificationsStore } from '../stores/notifications';
96
95
  import { capitalize } from 'lodash';
97
- import { fileSizeFormat } from 'src/utils';
96
+ import BaseFileUploader from './BaseFileUploader.vue';
97
+ import BaseMediaItem from './BaseMediaItem.vue';
98
+ import { fileSizeFormat } from '@/utils';
99
+ import BaseAlert from './BaseAlert.vue';
98
100
 
99
101
  const i18n = useI18n();
100
102
 
@@ -146,7 +148,7 @@ const props = defineProps({
146
148
  },
147
149
  errors: {
148
150
  default: undefined,
149
- type: Object as PropType<Record<string, string[]>>,
151
+ type: [Array] as PropType<string[]>,
150
152
  },
151
153
  disabled: {
152
154
  default: false,
@@ -159,9 +161,9 @@ const currentMediaInternal = ref(cloneDeep(props.currentMedia));
159
161
  const normalizedModelValue = computed(() => {
160
162
  if (
161
163
  props.modelValue &&
162
- _.isObject(props.modelValue) &&
163
- _.isArray(props.modelValue.to_add) &&
164
- _.isArray(props.modelValue.to_remove)
164
+ isObject(props.modelValue) &&
165
+ isArray(props.modelValue.to_add) &&
166
+ isArray(props.modelValue.to_remove)
165
167
  ) {
166
168
  return props.modelValue;
167
169
  }
@@ -260,23 +262,20 @@ function sync(modelValue: MediaLibraryPayload) {
260
262
  emit('update', modelValue);
261
263
  }
262
264
 
263
- function errorMessage(name: string): string {
264
- const errors = get(props.errors, name, []);
265
-
266
- if (errors.length == 0) {
267
- return '';
268
- }
269
-
270
- return errors[0];
271
- }
272
-
273
265
  const globalErrorMessage = computed(() => {
274
- if (errorMessage(props.name + '.to_add.0')) {
275
- return errorMessage(props.name + '.to_add.0');
276
- }
277
- if (errorMessage(props.name + '.to_remove')) {
278
- return errorMessage(props.name + '.to_remove');
266
+ if (props.errors && props.errors.length) {
267
+ return props.errors[0];
279
268
  }
280
269
  return '';
281
270
  });
271
+
272
+ const maxFileText = computed(() => {
273
+ return i18n.t('sui.you_can_upload_up_to_n_files', { count: props.max });
274
+ });
275
+
276
+ const maxFileSize = computed(() => {
277
+ return capitalize(
278
+ i18n.t('sui.up_to_x', { x: fileSizeFormat(props.maxSize) })
279
+ );
280
+ });
282
281
  </script>
@@ -0,0 +1,72 @@
1
+ import BaseMediaPreview from './BaseMediaPreview.vue';
2
+
3
+ export default {
4
+ title: 'Components/BaseMediaPreview',
5
+ component: BaseMediaPreview,
6
+ };
7
+
8
+ const Template = (args) => ({
9
+ components: {
10
+ BaseMediaPreview,
11
+ },
12
+ setup() {
13
+ return { args };
14
+ },
15
+ template: `
16
+ <BaseMediaPreview v-bind="args" class="w-20 h-20" />
17
+ `,
18
+ });
19
+
20
+ export const Image = Template.bind({});
21
+ Image.args = {
22
+ media: {
23
+ id: 'xxxxxxxxxxx',
24
+ file_name: 'picture.jpg',
25
+ mime_type: 'image/jpg',
26
+ size: 430 * 1024,
27
+ },
28
+ };
29
+
30
+ export const ImageUrl = Template.bind({});
31
+ ImageUrl.args = {
32
+ media: {
33
+ id: 'xxxxxxxxxxx',
34
+ file_name: 'picture.jpg',
35
+ mime_type: 'image/jpg',
36
+ url: 'https://images.unsplash.com/photo-1670139018938-af8bf748a1bc?auto=format&fit=crop&w=800&h=800&q=80',
37
+ size: 430 * 1024,
38
+ },
39
+ };
40
+
41
+ export const PDF = Template.bind({});
42
+ PDF.args = {
43
+ media: {
44
+ id: 'xxxxxxxxxxx',
45
+ file_name: 'picture.pdf',
46
+ mime_type: 'application/pdf',
47
+ url: 'https://images.unsplash.com/photo-1670139018938-af8bf748a1bc?auto=format&fit=crop&w=800&h=800&q=80',
48
+ size: 430 * 1024,
49
+ },
50
+ };
51
+
52
+ export const Music = Template.bind({});
53
+ Music.args = {
54
+ media: {
55
+ id: 'xxxxxxxxxxx',
56
+ file_name: 'picture.mp3',
57
+ mime_type: 'audio/mpeg',
58
+ url: 'https://images.unsplash.com/photo-1670139018938-af8bf748a1bc?auto=format&fit=crop&w=800&h=800&q=80',
59
+ size: 430 * 1024,
60
+ },
61
+ };
62
+
63
+ export const Other = Template.bind({});
64
+ Other.args = {
65
+ media: {
66
+ id: 'xxxxxxxxxxx',
67
+ file_name: 'picture.mp3',
68
+ mime_type: 'application/excel',
69
+ url: 'https://images.unsplash.com/photo-1670139018938-af8bf748a1bc?auto=format&fit=crop&w=800&h=800&q=80',
70
+ size: 430 * 1024,
71
+ },
72
+ };
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <component
3
+ :is="url ? 'a' : 'div'"
4
+ :href="url"
5
+ target="_blank"
6
+ class="relative flex items-center justify-center overflow-hidden"
7
+ :class="[url ? 'duration-100 hover:bg-slate-100' : 'bg-white']"
8
+ >
9
+ <img
10
+ v-if="type == 'image' && url"
11
+ :src="url"
12
+ class="h-full w-full bg-black object-contain object-center"
13
+ :alt="name"
14
+ />
15
+ <img
16
+ v-else-if="type == 'image' && 'data_url' in media"
17
+ :src="media.data_url"
18
+ class="h-full w-full bg-black object-contain object-center"
19
+ :alt="name"
20
+ />
21
+ <div
22
+ v-else
23
+ class="flex h-full w-full items-center justify-center bg-slate-200"
24
+ >
25
+ <BaseIcon
26
+ v-if="extension == 'pdf'"
27
+ class="max-w-8 h-1/2 max-h-8 w-1/2 text-slate-600"
28
+ icon="mdi:file-pdf-box"
29
+ />
30
+ <BaseIcon
31
+ v-else-if="type == 'image'"
32
+ class="max-w-8 h-1/2 max-h-8 w-1/2 text-slate-600"
33
+ icon="mdi:camera"
34
+ />
35
+ <BaseIcon
36
+ v-else-if="type == 'audio'"
37
+ class="max-w-8 h-1/2 max-h-8 w-1/2 text-slate-600"
38
+ icon="mdi:music"
39
+ />
40
+ <span
41
+ v-else
42
+ class="text-xs font-semibold uppercase leading-tight text-slate-600"
43
+ >
44
+ {{ extension }}
45
+ </span>
46
+ </div>
47
+ </component>
48
+ </template>
49
+
50
+ <script lang="ts" setup>
51
+ import { Media } from '@/types/Media';
52
+ import { UploadedFile } from '@/types/UploadedFile';
53
+ import { PropType } from 'vue';
54
+ import { BaseIcon } from './index';
55
+
56
+ defineEmits(['delete']);
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
+ });
68
+
69
+ const name = computed(() => {
70
+ return props.media.file_name;
71
+ });
72
+
73
+ const type = computed(() => {
74
+ const parts = props.media.mime_type.split('/');
75
+ return parts[0];
76
+ });
77
+
78
+ const extension = computed(() => {
79
+ const parts = props.media.mime_type.split('/');
80
+ return parts[parts.length - 1];
81
+ });
82
+
83
+ const url = computed(() => {
84
+ if ('url' in props.media) {
85
+ return props.media.url;
86
+ }
87
+
88
+ return null;
89
+ });
90
+ </script>
@@ -16,7 +16,7 @@
16
16
  >
17
17
  <MenuItems
18
18
  :class="[menuClass, menuPositionClass]"
19
- class="absolute z-10 mt-2 rounded-md bg-white p-1 shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
19
+ class="absolute z-menu mt-2 rounded-md bg-white p-1 shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
20
20
  >
21
21
  <slot name="items">
22
22
  <template v-for="item in items" :key="item.label + 'link'">
@@ -36,7 +36,7 @@ const EMPTY_VALUE_EXTERNAL = null;
36
36
  const props = defineProps({
37
37
  modelValue: {
38
38
  default: undefined,
39
- type: [String, Number, null, undefined] as PropType<Option>,
39
+ type: [String, Number, null] as PropType<Option | undefined>,
40
40
  },
41
41
  name: {
42
42
  default: undefined,
@@ -1,11 +1,15 @@
1
1
  <template>
2
- <router-link v-slot="{ href, navigate, isActive }" :to="to" custom>
2
+ <router-link
3
+ v-slot="{ href, navigate, isActive, isExactActive }"
4
+ :to="to"
5
+ custom
6
+ >
3
7
  <a
4
8
  :href="disabled ? undefined : href"
5
9
  :disabled="disabled"
6
10
  class="group relative flex items-center px-3 py-1"
7
11
  :class="[
8
- isActive
12
+ (activeStrategy == 'default' ? isActive : isExactActive)
9
13
  ? 'font-semibold text-blue-600'
10
14
  : 'text-slate-600 hover:text-slate-900',
11
15
  disabled ? 'cursor-not-allowed opacity-60' : '',
@@ -15,7 +19,7 @@
15
19
  <div
16
20
  class="absolute left-0 top-0 h-full"
17
21
  :class="[
18
- isActive
22
+ (activeStrategy == 'default' ? isActive : isExactActive)
19
23
  ? 'w-[2px] bg-blue-600'
20
24
  : 'group-hover:w-px group-hover:bg-slate-700',
21
25
  ]"
@@ -38,6 +42,10 @@ const props = defineProps({
38
42
  default: false,
39
43
  type: Boolean,
40
44
  },
45
+ activeStrategy: {
46
+ default: 'default',
47
+ type: String as PropType<'default' | 'exact'>,
48
+ },
41
49
  });
42
50
 
43
51
  const onClick = (navigate: () => void) => {
@@ -36,7 +36,7 @@ import { RouteLocationRaw } from 'vue-router';
36
36
  const props = defineProps({
37
37
  to: {
38
38
  default: undefined,
39
- type: [Object, String, undefined] as PropType<RouteLocationRaw | undefined>,
39
+ type: [Object, String] as PropType<RouteLocationRaw | undefined>,
40
40
  },
41
41
  action: {
42
42
  default: undefined,