nuxt-media-library 0.0.44 → 0.0.49

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 (111) hide show
  1. package/README.md +71 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +4 -1
  4. package/dist/runtime/components/MLAssetManager/Processing.vue +3 -1
  5. package/dist/runtime/components/MLMedia/Dropzone.d.vue.ts +60 -0
  6. package/dist/runtime/components/MLMedia/Dropzone.vue +137 -0
  7. package/dist/runtime/components/MLMedia/Dropzone.vue.d.ts +60 -0
  8. package/dist/runtime/components/MLMedia/Editor.d.vue.ts +23 -0
  9. package/dist/runtime/components/MLMedia/Editor.vue +34 -0
  10. package/dist/runtime/components/MLMedia/Editor.vue.d.ts +23 -0
  11. package/dist/runtime/components/MLMedia/EditorPanel.d.vue.ts +38 -0
  12. package/dist/runtime/components/MLMedia/EditorPanel.vue +220 -0
  13. package/dist/runtime/components/MLMedia/EditorPanel.vue.d.ts +38 -0
  14. package/dist/runtime/components/MLMedia/Field.d.vue.ts +65 -0
  15. package/dist/runtime/components/MLMedia/Field.vue +282 -0
  16. package/dist/runtime/components/MLMedia/Field.vue.d.ts +65 -0
  17. package/dist/runtime/components/MLMedia/FileFacts.d.vue.ts +19 -0
  18. package/dist/runtime/components/MLMedia/FileFacts.vue +38 -0
  19. package/dist/runtime/components/MLMedia/FileFacts.vue.d.ts +19 -0
  20. package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +23 -0
  21. package/dist/runtime/components/MLMedia/FilePicker.vue +33 -0
  22. package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +23 -0
  23. package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +33 -0
  24. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +300 -0
  25. package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +33 -0
  26. package/dist/runtime/components/MLMedia/FilterPill.d.vue.ts +30 -0
  27. package/dist/runtime/components/MLMedia/FilterPill.vue +84 -0
  28. package/dist/runtime/components/MLMedia/FilterPill.vue.d.ts +30 -0
  29. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +78 -0
  30. package/dist/runtime/components/MLMedia/Gallery.vue +256 -0
  31. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +78 -0
  32. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +102 -0
  33. package/dist/runtime/components/MLMedia/GallerySection.vue +422 -0
  34. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +102 -0
  35. package/dist/runtime/components/MLMedia/KindBadge.d.vue.ts +17 -0
  36. package/dist/runtime/components/MLMedia/KindBadge.vue +22 -0
  37. package/dist/runtime/components/MLMedia/KindBadge.vue.d.ts +17 -0
  38. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +63 -0
  39. package/dist/runtime/components/MLMedia/MediaTile.vue +117 -0
  40. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +63 -0
  41. package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +42 -0
  42. package/dist/runtime/components/MLMedia/PickerCard.vue +79 -0
  43. package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +42 -0
  44. package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +31 -0
  45. package/dist/runtime/components/MLMedia/PickerRow.vue +65 -0
  46. package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +31 -0
  47. package/dist/runtime/components/MLMedia/PreviewEye.d.vue.ts +16 -0
  48. package/dist/runtime/components/MLMedia/PreviewEye.vue +21 -0
  49. package/dist/runtime/components/MLMedia/PreviewEye.vue.d.ts +16 -0
  50. package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +25 -0
  51. package/dist/runtime/components/MLMedia/ProgressVeil.vue +98 -0
  52. package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +25 -0
  53. package/dist/runtime/components/MLMedia/StatusBadge.d.vue.ts +17 -0
  54. package/dist/runtime/components/MLMedia/StatusBadge.vue +31 -0
  55. package/dist/runtime/components/MLMedia/StatusBadge.vue.d.ts +17 -0
  56. package/dist/runtime/components/MLMedia/Thumbnail.d.vue.ts +36 -0
  57. package/dist/runtime/components/MLMedia/Thumbnail.vue +84 -0
  58. package/dist/runtime/components/MLMedia/Thumbnail.vue.d.ts +36 -0
  59. package/dist/runtime/components/MLMedia/UsageList.d.vue.ts +26 -0
  60. package/dist/runtime/components/MLMedia/UsageList.vue +66 -0
  61. package/dist/runtime/components/MLMedia/UsageList.vue.d.ts +26 -0
  62. package/dist/runtime/components/MLProcessing/StepLayout.d.vue.ts +1 -1
  63. package/dist/runtime/components/MLProcessing/StepLayout.vue.d.ts +1 -1
  64. package/dist/runtime/components/MLUploader/Item.d.vue.ts +2 -2
  65. package/dist/runtime/components/MLUploader/Item.vue.d.ts +2 -2
  66. package/dist/runtime/composables/useMLAssetPreview.js +17 -1
  67. package/dist/runtime/composables/useMLFilePicker.d.ts +20 -0
  68. package/dist/runtime/composables/useMLFilePicker.js +17 -0
  69. package/dist/runtime/composables/useMLMediaAssets.d.ts +19 -0
  70. package/dist/runtime/composables/useMLMediaAssets.js +56 -0
  71. package/dist/runtime/composables/useMLMediaBrowse.d.ts +147 -0
  72. package/dist/runtime/composables/useMLMediaBrowse.js +72 -0
  73. package/dist/runtime/composables/useMLMediaEditor.d.ts +22 -0
  74. package/dist/runtime/composables/useMLMediaEditor.js +20 -0
  75. package/dist/runtime/composables/useMLMediaField.d.ts +29 -0
  76. package/dist/runtime/composables/useMLMediaField.js +77 -0
  77. package/dist/runtime/composables/useMLMediaGallery.d.ts +55 -0
  78. package/dist/runtime/composables/useMLMediaGallery.js +134 -0
  79. package/dist/runtime/composables/useMLMediaGrants.d.ts +23 -0
  80. package/dist/runtime/composables/useMLMediaGrants.js +22 -0
  81. package/dist/runtime/composables/useMLMediaUpload.d.ts +134 -0
  82. package/dist/runtime/composables/useMLMediaUpload.js +153 -0
  83. package/dist/runtime/composables/useMLServiceUpload.d.ts +36 -0
  84. package/dist/runtime/composables/useMLServiceUpload.js +32 -0
  85. package/dist/runtime/index.d.ts +11 -0
  86. package/dist/runtime/index.js +11 -0
  87. package/dist/runtime/server/create-ml-proxy.d.ts +16 -0
  88. package/dist/runtime/server/create-ml-proxy.js +138 -40
  89. package/dist/runtime/server/grants.d.ts +124 -0
  90. package/dist/runtime/server/grants.js +161 -0
  91. package/dist/runtime/server/index.d.ts +2 -0
  92. package/dist/runtime/server/index.js +7 -0
  93. package/dist/runtime/server/uploads-proxy.js +5 -2
  94. package/dist/runtime/types/asset.d.ts +3 -1
  95. package/dist/runtime/utils/assetDisplay.d.ts +8 -0
  96. package/dist/runtime/utils/assetDisplay.js +29 -0
  97. package/dist/runtime/utils/assetFormat.d.ts +30 -0
  98. package/dist/runtime/utils/assetFormat.js +69 -0
  99. package/dist/runtime/utils/assetThumbnail.d.ts +11 -0
  100. package/dist/runtime/utils/assetThumbnail.js +14 -0
  101. package/dist/runtime/utils/fileAccept.d.ts +26 -0
  102. package/dist/runtime/utils/fileAccept.js +104 -0
  103. package/dist/runtime/utils/galleryDrag.d.ts +2 -0
  104. package/dist/runtime/utils/galleryDrag.js +1 -0
  105. package/dist/runtime/utils/grants/listeners.d.ts +2 -0
  106. package/dist/runtime/utils/grants/listeners.js +8 -0
  107. package/dist/runtime/utils/grants/store.d.ts +26 -0
  108. package/dist/runtime/utils/grants/store.js +37 -0
  109. package/dist/runtime/utils/uploader/serviceUpload.d.ts +84 -0
  110. package/dist/runtime/utils/uploader/serviceUpload.js +109 -0
  111. package/package.json +5 -2
@@ -0,0 +1,220 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import { useMLMediaAssets } from "../../composables/useMLMediaAssets";
4
+ import { assetName } from "../../utils/assetFormat";
5
+ const props = defineProps({
6
+ assetId: { type: String, required: true },
7
+ siblings: { type: Array, required: false, default: () => [] },
8
+ alt: { type: Object, required: false, default: () => ({}) },
9
+ readonly: { type: Boolean, required: false, default: false },
10
+ removeLabel: { type: String, required: false, default: "" },
11
+ usages: { type: Object, required: false, default: void 0 }
12
+ });
13
+ const emit = defineEmits(["save", "remove", "cancel"]);
14
+ const list = computed(() => {
15
+ const s = props.siblings.filter(Boolean);
16
+ return s.includes(props.assetId) ? s : [props.assetId, ...s];
17
+ });
18
+ const index = ref(Math.max(0, list.value.indexOf(props.assetId)));
19
+ const currentId = computed(() => list.value[index.value] ?? props.assetId);
20
+ const library = useMLMediaAssets(list);
21
+ const asset = computed(() => library.get(currentId.value));
22
+ const lookup = computed(() => library.lookup(currentId.value));
23
+ const name = computed(() => asset.value ? assetName(asset.value) : "");
24
+ const kind = computed(() => (asset.value?.kind ?? "").toLowerCase());
25
+ const takesAlt = computed(() => kind.value === "image" || kind.value === "video");
26
+ const alts = ref({ ...props.alt });
27
+ const dirty = computed(() => list.value.some((id) => (alts.value[id] ?? "") !== (props.alt[id] ?? "")));
28
+ const altModel = computed({
29
+ get: () => alts.value[currentId.value] ?? "",
30
+ set: (v) => {
31
+ alts.value = { ...alts.value, [currentId.value]: v };
32
+ }
33
+ });
34
+ const canPrev = computed(() => index.value > 0);
35
+ const canNext = computed(() => index.value < list.value.length - 1);
36
+ const go = (step) => {
37
+ index.value = Math.min(list.value.length - 1, Math.max(0, index.value + step));
38
+ };
39
+ function onKey(e) {
40
+ const t = e.target;
41
+ if (t && (t.tagName === "TEXTAREA" || t.tagName === "INPUT" || t.isContentEditable)) return;
42
+ if (e.key === "ArrowLeft" && canPrev.value) go(-1);
43
+ else if (e.key === "ArrowRight" && canNext.value) go(1);
44
+ }
45
+ const confirmRemove = ref(false);
46
+ watch(currentId, () => {
47
+ confirmRemove.value = false;
48
+ });
49
+ const usagesHere = computed(() => props.usages?.[currentId.value]);
50
+ const originalUrl = computed(() => asset.value?.previewUrl ?? null);
51
+ defineExpose({ currentId });
52
+ </script>
53
+
54
+ <template>
55
+ <div
56
+ class="flex min-h-0 flex-col"
57
+ @keydown="onKey"
58
+ >
59
+ <div class="grid min-h-0 md:grid-cols-[minmax(0,1fr)_18rem]">
60
+ <!-- Stage -->
61
+ <div class="relative grid min-h-72 place-items-center bg-neutral-950 px-12 py-10 md:min-h-[26rem]">
62
+ <USkeleton
63
+ v-if="lookup === 'loading'"
64
+ class="aspect-[4/3] w-full max-w-lg rounded-md bg-white/10"
65
+ />
66
+ <div
67
+ v-else-if="!asset"
68
+ class="flex flex-col items-center gap-2 text-center text-sm text-neutral-300"
69
+ >
70
+ <UIcon
71
+ name="i-lucide-file-x"
72
+ class="size-8 text-neutral-400"
73
+ />
74
+ {{ lookup === "forbidden" ? "You don't have access to this file." : "This file could not be found." }}
75
+ </div>
76
+ <div
77
+ v-else
78
+ :key="currentId"
79
+ class="h-[min(52vh,26rem)] w-full max-w-2xl"
80
+ >
81
+ <MLAssetManagerAssetPreview :asset="asset" />
82
+ </div>
83
+
84
+ <UButton
85
+ v-if="list.length > 1"
86
+ icon="i-lucide-chevron-left"
87
+ color="neutral"
88
+ variant="soft"
89
+ class="absolute left-2 top-1/2 -translate-y-1/2 rounded-full"
90
+ aria-label="Previous file"
91
+ :disabled="!canPrev"
92
+ @click="go(-1)"
93
+ />
94
+ <UButton
95
+ v-if="list.length > 1"
96
+ icon="i-lucide-chevron-right"
97
+ color="neutral"
98
+ variant="soft"
99
+ class="absolute right-2 top-1/2 -translate-y-1/2 rounded-full"
100
+ aria-label="Next file"
101
+ :disabled="!canNext"
102
+ @click="go(1)"
103
+ />
104
+ <span
105
+ v-if="list.length > 1"
106
+ class="absolute bottom-2.5 left-1/2 -translate-x-1/2 text-xs text-neutral-300 tabular-nums"
107
+ aria-live="polite"
108
+ >{{ index + 1 }} of {{ list.length }}</span>
109
+ </div>
110
+
111
+ <!-- Side panel -->
112
+ <div class="flex min-w-0 flex-col gap-5 border-t border-default p-4 md:border-l md:border-t-0">
113
+ <div class="flex min-w-0 items-start gap-2">
114
+ <div class="flex min-w-0 flex-1 flex-col gap-1">
115
+ <span class="truncate text-sm font-semibold">{{ name || "\u2014" }}</span>
116
+ <div
117
+ v-if="asset"
118
+ class="flex flex-wrap gap-1"
119
+ >
120
+ <MLMediaKindBadge :kind="asset.kind" />
121
+ <MLMediaStatusBadge
122
+ v-if="asset.status !== 'ready'"
123
+ :status="asset.status"
124
+ />
125
+ </div>
126
+ </div>
127
+ <UButton
128
+ v-if="originalUrl"
129
+ :to="originalUrl"
130
+ target="_blank"
131
+ rel="noopener"
132
+ icon="i-lucide-external-link"
133
+ color="neutral"
134
+ variant="ghost"
135
+ size="sm"
136
+ aria-label="Open the original in a new tab"
137
+ />
138
+ </div>
139
+
140
+ <UFormField
141
+ v-if="takesAlt && !readonly"
142
+ label="Alt text"
143
+ :help="kind === 'image' ? 'Describes the image for screen readers and search.' : 'Describes the video for screen readers.'"
144
+ >
145
+ <UTextarea
146
+ v-model="altModel"
147
+ :rows="3"
148
+ autoresize
149
+ class="w-full"
150
+ :placeholder="kind === 'image' ? 'e.g. Oak lounge chair, front view on a light background' : ''"
151
+ />
152
+ </UFormField>
153
+ <p
154
+ v-else-if="takesAlt && altModel"
155
+ class="text-sm"
156
+ >
157
+ <span class="block text-[11px] font-semibold uppercase tracking-wider text-dimmed">Alt text</span>
158
+ {{ altModel }}
159
+ </p>
160
+
161
+ <MLMediaFileFacts :asset="asset" />
162
+
163
+ <MLMediaUsageList
164
+ v-if="usagesHere"
165
+ :usages="usagesHere"
166
+ />
167
+ </div>
168
+ </div>
169
+
170
+ <div class="flex flex-wrap items-center gap-2 border-t border-default px-4 py-3">
171
+ <template v-if="removeLabel && !readonly">
172
+ <UButton
173
+ v-if="!confirmRemove"
174
+ color="error"
175
+ variant="ghost"
176
+ icon="i-lucide-x"
177
+ :label="removeLabel"
178
+ @click="confirmRemove = true"
179
+ />
180
+ <template v-else>
181
+ <span class="text-sm text-muted">The file stays in the library.</span>
182
+ <UButton
183
+ color="error"
184
+ variant="soft"
185
+ :label="removeLabel"
186
+ @click="emit('remove', currentId)"
187
+ />
188
+ <UButton
189
+ color="neutral"
190
+ variant="ghost"
191
+ label="Keep"
192
+ @click="confirmRemove = false"
193
+ />
194
+ </template>
195
+ </template>
196
+ <span class="me-auto" />
197
+ <template v-if="readonly">
198
+ <UButton
199
+ color="neutral"
200
+ variant="outline"
201
+ label="Close"
202
+ @click="emit('cancel')"
203
+ />
204
+ </template>
205
+ <template v-else>
206
+ <UButton
207
+ color="neutral"
208
+ variant="outline"
209
+ label="Cancel"
210
+ @click="emit('cancel')"
211
+ />
212
+ <UButton
213
+ label="Save"
214
+ :disabled="!dirty"
215
+ @click="emit('save', { alt: { ...alts } })"
216
+ />
217
+ </template>
218
+ </div>
219
+ </div>
220
+ </template>
@@ -0,0 +1,38 @@
1
+ import type { MediaUsage } from './UsageList.vue.js';
2
+ type __VLS_Props = {
3
+ /** The file to show first. */
4
+ assetId: string;
5
+ /** The record's other files, in order. Enables previous / next. */
6
+ siblings?: readonly string[];
7
+ /** Current alt text per asset ID. Alt text lives on the host record, not the file. */
8
+ alt?: Readonly<Record<string, string>>;
9
+ /** Preview only: no alt editing, no Save. Used from the picker. */
10
+ readonly?: boolean;
11
+ /** Label for the remove button, e.g. "Remove from product". Empty hides it. */
12
+ removeLabel?: string;
13
+ /** Where each file is used, per asset ID. Omit to hide the section. */
14
+ usages?: Readonly<Record<string, MediaUsage[]>>;
15
+ };
16
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
17
+ currentId: import("vue").ComputedRef<string>;
18
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ remove: (assetId: string) => any;
20
+ cancel: () => any;
21
+ save: (result: {
22
+ alt: Record<string, string>;
23
+ }) => any;
24
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
+ onRemove?: ((assetId: string) => any) | undefined;
26
+ onCancel?: (() => any) | undefined;
27
+ onSave?: ((result: {
28
+ alt: Record<string, string>;
29
+ }) => any) | undefined;
30
+ }>, {
31
+ usages: Readonly<Record<string, MediaUsage[]>>;
32
+ siblings: readonly string[];
33
+ alt: Readonly<Record<string, string>>;
34
+ readonly: boolean;
35
+ removeLabel: string;
36
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
37
+ declare const _default: typeof __VLS_export;
38
+ export default _default;
@@ -0,0 +1,65 @@
1
+ import type { MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
2
+ type __VLS_Props = {
3
+ /** Asset kinds the field takes: ['image'], "image,video". Empty means any file. */
4
+ accept?: readonly string[] | string;
5
+ label?: string;
6
+ /** Under the field. Defaults to what the field accepts and its size limit. */
7
+ hint?: string;
8
+ required?: boolean;
9
+ disabled?: boolean;
10
+ /** Bytes. */
11
+ maxSize?: number | null;
12
+ /** Shows "No alt text" on a filled image field. Alt text lives on the host record. */
13
+ missingAlt?: boolean;
14
+ /** Upload session settings for new files. */
15
+ visibility?: MediaUploadOptions['visibility'];
16
+ processingConfig?: MediaUploadOptions['processingConfig'];
17
+ collectionName?: string | null;
18
+ /** Show "Select existing", which opens the library picker. */
19
+ picker?: boolean;
20
+ /** Replaces the built-in picker, e.g. with the host's own dialog. */
21
+ onPick?: () => void;
22
+ /** Show the eye button on the thumbnail, which opens the media editor. */
23
+ editor?: boolean;
24
+ /** Label for the editor's remove button. Empty hides it. */
25
+ removeLabel?: string;
26
+ /** Replaces the built-in editor. Receives the asset ID. */
27
+ onOpen?: (assetId: string) => void;
28
+ };
29
+ type __VLS_ModelProps = {
30
+ modelValue?: string | null;
31
+ /** Alt text for the file, stored on the host record. Edited in the media editor. */
32
+ 'alt'?: string | undefined;
33
+ };
34
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
35
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
36
+ uploaded: (assetId: string) => any;
37
+ "update:modelValue": (value: string | null) => any;
38
+ "update:alt": (value: string | undefined) => any;
39
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
40
+ onUploaded?: ((assetId: string) => any) | undefined;
41
+ "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
42
+ "onUpdate:alt"?: ((value: string | undefined) => any) | undefined;
43
+ }>, {
44
+ visibility: "private" | "restricted" | "public";
45
+ label: string;
46
+ accept: readonly string[] | string;
47
+ maxSize: number | null;
48
+ processingConfig: {
49
+ stepOptions?: Record<string, Record<string, unknown>> | null;
50
+ useFilenameAsAssetId?: boolean;
51
+ enableTranscription?: boolean;
52
+ };
53
+ collectionName: string | null;
54
+ disabled: boolean;
55
+ required: boolean;
56
+ removeLabel: string;
57
+ hint: string;
58
+ missingAlt: boolean;
59
+ picker: boolean;
60
+ onPick: () => void;
61
+ editor: boolean;
62
+ onOpen: (assetId: string) => void;
63
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
64
+ declare const _default: typeof __VLS_export;
65
+ export default _default;
@@ -0,0 +1,282 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useMLMediaField } from "../../composables/useMLMediaField";
4
+ import { useMLFilePicker } from "../../composables/useMLFilePicker";
5
+ import { useMLMediaEditor } from "../../composables/useMLMediaEditor";
6
+ import { assetDimensions, assetDurationSeconds, assetName, assetSizeBytes, formatBytes, formatDuration } from "../../utils/assetFormat";
7
+ const props = defineProps({
8
+ accept: { type: [Array, String], required: false, default: "" },
9
+ label: { type: String, required: false, default: void 0 },
10
+ hint: { type: String, required: false, default: void 0 },
11
+ required: { type: Boolean, required: false, default: false },
12
+ disabled: { type: Boolean, required: false, default: false },
13
+ maxSize: { type: [Number, null], required: false, default: null },
14
+ missingAlt: { type: Boolean, required: false, default: false },
15
+ visibility: { type: String, required: false, default: void 0 },
16
+ processingConfig: { type: Object, required: false, default: void 0 },
17
+ collectionName: { type: [String, null], required: false, default: null },
18
+ picker: { type: Boolean, required: false, default: true },
19
+ onPick: { type: Function, required: false, default: void 0 },
20
+ editor: { type: Boolean, required: false, default: true },
21
+ removeLabel: { type: String, required: false, default: "Remove from this field" },
22
+ onOpen: { type: Function, required: false, default: void 0 }
23
+ });
24
+ const model = defineModel({ type: [String, null], ...{ default: null } });
25
+ const alt = defineModel("alt", { type: null, ...{ default: void 0 } });
26
+ const emit = defineEmits(["uploaded"]);
27
+ const field = useMLMediaField(model, {
28
+ accept: () => props.accept,
29
+ maxSize: () => props.maxSize,
30
+ visibility: props.visibility,
31
+ processingConfig: props.processingConfig,
32
+ collectionName: props.collectionName,
33
+ onUploaded: (id) => emit("uploaded", id)
34
+ });
35
+ const { state, asset, upload, message } = field;
36
+ const helpText = computed(() => {
37
+ if (props.hint) return props.hint;
38
+ const parts = [field.acceptLabel.value === "any file" ? "" : `Takes ${field.acceptLabel.value}`];
39
+ if (props.maxSize) parts.push(`up to ${formatBytes(props.maxSize)}`);
40
+ return parts.filter(Boolean).join(" ").replace(/^./, (c) => c.toUpperCase());
41
+ });
42
+ const name = computed(() => upload.value?.file.name ?? assetName(asset.value));
43
+ const kind = computed(() => upload.value?.kind ?? asset.value?.kind ?? null);
44
+ const isImage = computed(() => (kind.value ?? "").toLowerCase() === "image");
45
+ const details = computed(() => {
46
+ const u = upload.value;
47
+ if (u && state.value === "uploading") {
48
+ if (u.phase === "failed") return u.error ?? "Upload failed";
49
+ if (u.phase === "processing") return u.progress != null ? `Processing ${Math.round(u.progress)}%` : "Processing\u2026";
50
+ return `Uploading ${Math.round(u.progress ?? 0)}% \xB7 ${formatBytes(u.file.size)}`;
51
+ }
52
+ const a = asset.value;
53
+ if (!a) return "";
54
+ const dims = assetDimensions(a);
55
+ const duration = assetDurationSeconds(a);
56
+ return [
57
+ formatBytes(assetSizeBytes(a)),
58
+ dims ? `${dims.width} \xD7 ${dims.height}` : "",
59
+ duration ? formatDuration(duration) : ""
60
+ ].filter(Boolean).join(" \xB7 ");
61
+ });
62
+ const veilPhase = computed(() => {
63
+ const p = upload.value?.phase;
64
+ return state.value === "uploading" && (p === "uploading" || p === "processing" || p === "failed") ? p : null;
65
+ });
66
+ const filePicker = useMLFilePicker();
67
+ const canPick = computed(() => !!props.onPick || props.picker);
68
+ async function pickExisting() {
69
+ if (props.onPick) return props.onPick();
70
+ const ids = await filePicker.open({
71
+ accept: props.accept,
72
+ max: 1,
73
+ selected: [model.value],
74
+ title: props.label ? `Select ${props.label.toLowerCase()}` : "Select file",
75
+ maxSize: props.maxSize,
76
+ upload: { visibility: props.visibility, processingConfig: props.processingConfig, collectionName: props.collectionName }
77
+ });
78
+ if (ids) {
79
+ field.clearMessage();
80
+ model.value = ids[0] ?? null;
81
+ }
82
+ }
83
+ const mediaEditor = useMLMediaEditor();
84
+ const canOpen = computed(() => state.value === "filled" && !!model.value && (!!props.onOpen || props.editor));
85
+ const showNoAlt = computed(() => isImage.value && (props.missingAlt || alt.value !== void 0 && !alt.value.trim()));
86
+ async function openEditor() {
87
+ const id = model.value;
88
+ if (!canOpen.value || !id) return;
89
+ if (props.onOpen) return props.onOpen(id);
90
+ const result = await mediaEditor.open(id, {
91
+ alt: alt.value ?? "",
92
+ readonly: props.disabled,
93
+ removeLabel: props.removeLabel
94
+ });
95
+ if (result?.action === "save") alt.value = result.alt[id] ?? "";
96
+ else if (result?.action === "remove") field.clear();
97
+ }
98
+ function changeItems(open) {
99
+ return [
100
+ { label: "Upload new", icon: "i-lucide-upload", onSelect: open },
101
+ { label: "Select existing", icon: "i-lucide-layout-grid", onSelect: () => void pickExisting() }
102
+ ];
103
+ }
104
+ </script>
105
+
106
+ <template>
107
+ <UFormField
108
+ :label="label"
109
+ :required="required"
110
+ :help="message ? void 0 : helpText || void 0"
111
+ :error="message ?? void 0"
112
+ >
113
+ <!-- Empty -->
114
+ <MLMediaDropzone
115
+ v-if="state === 'empty'"
116
+ :accept="field.acceptAttr.value"
117
+ :multiple="false"
118
+ :max-size="maxSize"
119
+ :disabled="disabled"
120
+ @files="field.onFiles"
121
+ @rejected="field.onRejected"
122
+ >
123
+ <template
124
+ v-if="canPick"
125
+ #actions
126
+ >
127
+ <UButton
128
+ size="sm"
129
+ color="neutral"
130
+ variant="outline"
131
+ icon="i-lucide-layout-grid"
132
+ label="Select existing"
133
+ :disabled="disabled"
134
+ @click="pickExisting"
135
+ />
136
+ </template>
137
+ </MLMediaDropzone>
138
+
139
+ <!-- Loading the current file -->
140
+ <div
141
+ v-else-if="state === 'loading'"
142
+ class="flex items-center gap-3 rounded-lg border border-default p-2"
143
+ >
144
+ <USkeleton class="aspect-[4/3] w-20 rounded-md" />
145
+ <div class="flex flex-1 flex-col gap-1.5">
146
+ <USkeleton class="h-3.5 w-2/3" />
147
+ <USkeleton class="h-3 w-1/3" />
148
+ </div>
149
+ </div>
150
+
151
+ <!-- Filled, uploading, or pointing at a file that is gone -->
152
+ <MLMediaDropzone
153
+ v-else
154
+ variant="overlay"
155
+ :accept="field.acceptAttr.value"
156
+ :multiple="false"
157
+ :max-size="maxSize"
158
+ :disabled="disabled || state === 'uploading'"
159
+ title="Drop to replace"
160
+ class="rounded-lg"
161
+ @files="field.onFiles"
162
+ @rejected="field.onRejected"
163
+ >
164
+ <template #default="{ open }">
165
+ <div
166
+ class="flex items-center gap-3 rounded-lg border p-2"
167
+ :class="state === 'missing' ? 'border-warning/50 bg-warning/5' : 'border-default bg-default'"
168
+ >
169
+ <div class="group relative shrink-0">
170
+ <MLMediaThumbnail
171
+ :asset="upload?.localPreview ? null : asset"
172
+ :src="upload?.localPreview ?? void 0"
173
+ :kind="kind"
174
+ size="md"
175
+ aspect="landscape"
176
+ >
177
+ <MLMediaProgressVeil
178
+ v-if="veilPhase"
179
+ :phase="veilPhase"
180
+ :progress="upload?.progress ?? null"
181
+ compact
182
+ />
183
+ </MLMediaThumbnail>
184
+ <MLMediaPreviewEye
185
+ v-if="canOpen"
186
+ :name="name"
187
+ class="absolute bottom-1 right-1 z-20"
188
+ @click="openEditor"
189
+ />
190
+ </div>
191
+
192
+ <div class="flex min-w-0 flex-1 flex-col gap-0.5">
193
+ <span
194
+ v-if="state === 'missing'"
195
+ class="text-sm font-semibold"
196
+ >File unavailable</span>
197
+ <span
198
+ v-else
199
+ class="truncate text-sm font-semibold"
200
+ >{{ name }}</span>
201
+ <span
202
+ v-if="state === 'missing'"
203
+ class="text-xs text-muted"
204
+ >It was deleted, or you don't have access to it.</span>
205
+ <span
206
+ v-else
207
+ class="truncate text-xs tabular-nums"
208
+ :class="upload?.phase === 'failed' ? 'text-error' : 'text-muted'"
209
+ >{{ details }}</span>
210
+ <span
211
+ v-if="state === 'filled' && showNoAlt"
212
+ class="text-xs text-warning"
213
+ >No alt text</span>
214
+ </div>
215
+
216
+ <div
217
+ v-if="!disabled"
218
+ class="flex shrink-0 items-center gap-0.5"
219
+ >
220
+ <template v-if="state === 'uploading'">
221
+ <UButton
222
+ v-if="upload?.phase === 'failed'"
223
+ size="sm"
224
+ color="neutral"
225
+ variant="ghost"
226
+ icon="i-lucide-rotate-ccw"
227
+ label="Retry"
228
+ @click="upload?.retry()"
229
+ />
230
+ <UButton
231
+ v-if="upload?.phase === 'failed'"
232
+ size="sm"
233
+ color="neutral"
234
+ variant="ghost"
235
+ icon="i-lucide-x"
236
+ aria-label="Remove"
237
+ @click="field.clear()"
238
+ />
239
+ <UButton
240
+ v-else
241
+ size="sm"
242
+ color="neutral"
243
+ variant="ghost"
244
+ label="Cancel"
245
+ @click="upload?.cancel()"
246
+ />
247
+ </template>
248
+ <template v-else>
249
+ <UDropdownMenu
250
+ v-if="canPick"
251
+ :items="changeItems(open)"
252
+ :content="{ align: 'end' }"
253
+ >
254
+ <UButton
255
+ size="sm"
256
+ variant="ghost"
257
+ label="Change"
258
+ trailing-icon="i-lucide-chevron-down"
259
+ />
260
+ </UDropdownMenu>
261
+ <UButton
262
+ v-else
263
+ size="sm"
264
+ variant="ghost"
265
+ label="Change"
266
+ @click="open"
267
+ />
268
+ <UButton
269
+ size="sm"
270
+ color="neutral"
271
+ variant="ghost"
272
+ icon="i-lucide-x"
273
+ :aria-label="`Remove ${name || 'file'}`"
274
+ @click="field.clear()"
275
+ />
276
+ </template>
277
+ </div>
278
+ </div>
279
+ </template>
280
+ </MLMediaDropzone>
281
+ </UFormField>
282
+ </template>
@@ -0,0 +1,65 @@
1
+ import type { MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
2
+ type __VLS_Props = {
3
+ /** Asset kinds the field takes: ['image'], "image,video". Empty means any file. */
4
+ accept?: readonly string[] | string;
5
+ label?: string;
6
+ /** Under the field. Defaults to what the field accepts and its size limit. */
7
+ hint?: string;
8
+ required?: boolean;
9
+ disabled?: boolean;
10
+ /** Bytes. */
11
+ maxSize?: number | null;
12
+ /** Shows "No alt text" on a filled image field. Alt text lives on the host record. */
13
+ missingAlt?: boolean;
14
+ /** Upload session settings for new files. */
15
+ visibility?: MediaUploadOptions['visibility'];
16
+ processingConfig?: MediaUploadOptions['processingConfig'];
17
+ collectionName?: string | null;
18
+ /** Show "Select existing", which opens the library picker. */
19
+ picker?: boolean;
20
+ /** Replaces the built-in picker, e.g. with the host's own dialog. */
21
+ onPick?: () => void;
22
+ /** Show the eye button on the thumbnail, which opens the media editor. */
23
+ editor?: boolean;
24
+ /** Label for the editor's remove button. Empty hides it. */
25
+ removeLabel?: string;
26
+ /** Replaces the built-in editor. Receives the asset ID. */
27
+ onOpen?: (assetId: string) => void;
28
+ };
29
+ type __VLS_ModelProps = {
30
+ modelValue?: string | null;
31
+ /** Alt text for the file, stored on the host record. Edited in the media editor. */
32
+ 'alt'?: string | undefined;
33
+ };
34
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
35
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
36
+ uploaded: (assetId: string) => any;
37
+ "update:modelValue": (value: string | null) => any;
38
+ "update:alt": (value: string | undefined) => any;
39
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
40
+ onUploaded?: ((assetId: string) => any) | undefined;
41
+ "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
42
+ "onUpdate:alt"?: ((value: string | undefined) => any) | undefined;
43
+ }>, {
44
+ visibility: "private" | "restricted" | "public";
45
+ label: string;
46
+ accept: readonly string[] | string;
47
+ maxSize: number | null;
48
+ processingConfig: {
49
+ stepOptions?: Record<string, Record<string, unknown>> | null;
50
+ useFilenameAsAssetId?: boolean;
51
+ enableTranscription?: boolean;
52
+ };
53
+ collectionName: string | null;
54
+ disabled: boolean;
55
+ required: boolean;
56
+ removeLabel: string;
57
+ hint: string;
58
+ missingAlt: boolean;
59
+ picker: boolean;
60
+ onPick: () => void;
61
+ editor: boolean;
62
+ onOpen: (assetId: string) => void;
63
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
64
+ declare const _default: typeof __VLS_export;
65
+ export default _default;