nuxt-media-library 0.0.48 → 0.0.50
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.
- package/README.md +45 -0
- package/dist/module.d.mts +14 -1
- package/dist/module.d.ts +14 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +11 -2
- package/dist/runtime/components/MLMedia/Dropzone.d.vue.ts +60 -0
- package/dist/runtime/components/MLMedia/Dropzone.vue +137 -0
- package/dist/runtime/components/MLMedia/Dropzone.vue.d.ts +60 -0
- package/dist/runtime/components/MLMedia/Editor.d.vue.ts +23 -0
- package/dist/runtime/components/MLMedia/Editor.vue +34 -0
- package/dist/runtime/components/MLMedia/Editor.vue.d.ts +23 -0
- package/dist/runtime/components/MLMedia/EditorPanel.d.vue.ts +38 -0
- package/dist/runtime/components/MLMedia/EditorPanel.vue +220 -0
- package/dist/runtime/components/MLMedia/EditorPanel.vue.d.ts +38 -0
- package/dist/runtime/components/MLMedia/Field.d.vue.ts +65 -0
- package/dist/runtime/components/MLMedia/Field.vue +282 -0
- package/dist/runtime/components/MLMedia/Field.vue.d.ts +65 -0
- package/dist/runtime/components/MLMedia/FileFacts.d.vue.ts +19 -0
- package/dist/runtime/components/MLMedia/FileFacts.vue +38 -0
- package/dist/runtime/components/MLMedia/FileFacts.vue.d.ts +19 -0
- package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +23 -0
- package/dist/runtime/components/MLMedia/FilePicker.vue +33 -0
- package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +23 -0
- package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +33 -0
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue +300 -0
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +33 -0
- package/dist/runtime/components/MLMedia/FilterPill.d.vue.ts +30 -0
- package/dist/runtime/components/MLMedia/FilterPill.vue +84 -0
- package/dist/runtime/components/MLMedia/FilterPill.vue.d.ts +30 -0
- package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +78 -0
- package/dist/runtime/components/MLMedia/Gallery.vue +256 -0
- package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +78 -0
- package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +102 -0
- package/dist/runtime/components/MLMedia/GallerySection.vue +422 -0
- package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +102 -0
- package/dist/runtime/components/MLMedia/KindBadge.d.vue.ts +17 -0
- package/dist/runtime/components/MLMedia/KindBadge.vue +22 -0
- package/dist/runtime/components/MLMedia/KindBadge.vue.d.ts +17 -0
- package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +63 -0
- package/dist/runtime/components/MLMedia/MediaTile.vue +117 -0
- package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +63 -0
- package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +42 -0
- package/dist/runtime/components/MLMedia/PickerCard.vue +79 -0
- package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +42 -0
- package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +31 -0
- package/dist/runtime/components/MLMedia/PickerRow.vue +65 -0
- package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +31 -0
- package/dist/runtime/components/MLMedia/PreviewEye.d.vue.ts +16 -0
- package/dist/runtime/components/MLMedia/PreviewEye.vue +21 -0
- package/dist/runtime/components/MLMedia/PreviewEye.vue.d.ts +16 -0
- package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +25 -0
- package/dist/runtime/components/MLMedia/ProgressVeil.vue +98 -0
- package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +25 -0
- package/dist/runtime/components/MLMedia/StatusBadge.d.vue.ts +17 -0
- package/dist/runtime/components/MLMedia/StatusBadge.vue +31 -0
- package/dist/runtime/components/MLMedia/StatusBadge.vue.d.ts +17 -0
- package/dist/runtime/components/MLMedia/Thumbnail.d.vue.ts +36 -0
- package/dist/runtime/components/MLMedia/Thumbnail.vue +84 -0
- package/dist/runtime/components/MLMedia/Thumbnail.vue.d.ts +36 -0
- package/dist/runtime/components/MLMedia/UsageList.d.vue.ts +26 -0
- package/dist/runtime/components/MLMedia/UsageList.vue +66 -0
- package/dist/runtime/components/MLMedia/UsageList.vue.d.ts +26 -0
- package/dist/runtime/components/MLProcessing/StepLayout.d.vue.ts +1 -1
- package/dist/runtime/components/MLProcessing/StepLayout.vue.d.ts +1 -1
- package/dist/runtime/composables/useMLFilePicker.d.ts +20 -0
- package/dist/runtime/composables/useMLFilePicker.js +17 -0
- package/dist/runtime/composables/useMLMediaAssets.d.ts +19 -0
- package/dist/runtime/composables/useMLMediaAssets.js +56 -0
- package/dist/runtime/composables/useMLMediaBrowse.d.ts +147 -0
- package/dist/runtime/composables/useMLMediaBrowse.js +72 -0
- package/dist/runtime/composables/useMLMediaEditor.d.ts +22 -0
- package/dist/runtime/composables/useMLMediaEditor.js +20 -0
- package/dist/runtime/composables/useMLMediaField.d.ts +29 -0
- package/dist/runtime/composables/useMLMediaField.js +77 -0
- package/dist/runtime/composables/useMLMediaGallery.d.ts +55 -0
- package/dist/runtime/composables/useMLMediaGallery.js +134 -0
- package/dist/runtime/composables/useMLMediaUpload.d.ts +134 -0
- package/dist/runtime/composables/useMLMediaUpload.js +153 -0
- package/dist/runtime/composables/useMLServiceUpload.d.ts +10 -2
- package/dist/runtime/composables/useMLServiceUpload.js +4 -1
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.js +11 -0
- package/dist/runtime/server/index.d.ts +2 -0
- package/dist/runtime/server/index.js +1 -0
- package/dist/runtime/server/service-uploads-proxy.d.ts +31 -0
- package/dist/runtime/server/service-uploads-proxy.js +61 -0
- package/dist/runtime/utils/assetDisplay.d.ts +8 -0
- package/dist/runtime/utils/assetDisplay.js +29 -0
- package/dist/runtime/utils/assetFormat.d.ts +30 -0
- package/dist/runtime/utils/assetFormat.js +69 -0
- package/dist/runtime/utils/assetThumbnail.d.ts +11 -0
- package/dist/runtime/utils/assetThumbnail.js +14 -0
- package/dist/runtime/utils/fileAccept.d.ts +26 -0
- package/dist/runtime/utils/fileAccept.js +104 -0
- package/dist/runtime/utils/galleryDrag.d.ts +2 -0
- package/dist/runtime/utils/galleryDrag.js +1 -0
- package/dist/runtime/utils/uploader/serviceUpload.d.ts +16 -2
- package/dist/runtime/utils/uploader/serviceUpload.js +5 -1
- package/dist/types.d.mts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,6 +153,51 @@ addMediaGrants(lesson.mediaGrant, profile.mediaGrant) // "mlg_…" or { grant, e
|
|
|
153
153
|
grants are added.
|
|
154
154
|
- Grants live in the browser only, never in server-side state.
|
|
155
155
|
|
|
156
|
+
### Uploads started by your backend services
|
|
157
|
+
|
|
158
|
+
When a backend service starts the upload (the Python client's
|
|
159
|
+
`create_upload_router`), the service creates the session and returns a grant
|
|
160
|
+
for it, and `useMLServiceUpload` sends the parts. Where the parts go is the
|
|
161
|
+
app's decision, not the service's:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// nuxt.config.ts
|
|
165
|
+
export default defineNuxtConfig({
|
|
166
|
+
mediaLibrary: {
|
|
167
|
+
baseURL: process.env.MEDIA_LIBRARY_URL, // server side, e.g. http://media-library.internal/v1
|
|
168
|
+
serviceUploads: { proxy: true }, // parts go through /api/media-library/service-uploads
|
|
169
|
+
// or, direct to the media library (needs CORS for the site):
|
|
170
|
+
// serviceUploads: { baseURL: 'https://media.example.com/v1' },
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const { upload, progress } = useMLServiceUpload()
|
|
177
|
+
const { sessionId, assetId } = await upload(file, {
|
|
178
|
+
start: '/api/videos/uploads',
|
|
179
|
+
body: { profile: 'lesson-video', context: { lessonId } },
|
|
180
|
+
})
|
|
181
|
+
await $fetch(`/api/videos/lessons/${lessonId}/video`, { method: 'PUT', body: { sessionId, assetId } })
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- The upload base is, in order: `upload(file, { baseURL })`,
|
|
185
|
+
`useMLServiceUpload({ baseURL })`, `serviceUploads.baseURL`
|
|
186
|
+
(`NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BASE_URL`), the pass-through
|
|
187
|
+
route when `serviceUploads.proxy` is on, and last the `uploadBaseUrl` an
|
|
188
|
+
older service returns. With none, `upload` fails before any part is sent.
|
|
189
|
+
- The pass-through route (`serviceUploads.proxy: true`) forwards only a
|
|
190
|
+
session's `GET`, parts `PUT`, `finalize` and `abort` to the private
|
|
191
|
+
`mediaLibrary.baseURL`. It requires the browser's `Bearer mlg_…` grant and
|
|
192
|
+
forwards it and `X-Tenant` untouched (the static `X-Tenant` when the
|
|
193
|
+
browser sends none). It never adds the service token, static auth headers
|
|
194
|
+
or a Nuxt-issued grant, drops cookies, and streams request bodies. Mount it
|
|
195
|
+
yourself with `createMLServiceUploadsProxy()` from
|
|
196
|
+
`nuxt-media-library/server` for another path.
|
|
197
|
+
- Don't send these uploads through `createMLCatchAllProxy` with `authorize`:
|
|
198
|
+
grant mode replaces the browser's grant with its own, and the service's
|
|
199
|
+
`claim_upload` rejects the session.
|
|
200
|
+
|
|
156
201
|
### Vue installation (non-Nuxt)
|
|
157
202
|
|
|
158
203
|
```bash
|
package/dist/module.d.mts
CHANGED
|
@@ -24,8 +24,21 @@ interface ModuleOptions {
|
|
|
24
24
|
final?: boolean;
|
|
25
25
|
};
|
|
26
26
|
headers?: Record<string, string> | (() => Record<string, string>);
|
|
27
|
+
/**
|
|
28
|
+
* Uploads started by the app's backend services (`useMLServiceUpload`).
|
|
29
|
+
* `proxy: true` adds `/api/media-library/service-uploads/**`, a same-origin
|
|
30
|
+
* pass-through that forwards the browser's media grant and nothing else.
|
|
31
|
+
* `baseURL` is where the browser sends those uploads (the media library's
|
|
32
|
+
* public API root with `/v1`, or a proxy route); with `proxy` it defaults
|
|
33
|
+
* to the pass-through route.
|
|
34
|
+
*/
|
|
35
|
+
serviceUploads?: {
|
|
36
|
+
proxy?: boolean;
|
|
37
|
+
baseURL?: string;
|
|
38
|
+
};
|
|
27
39
|
}
|
|
40
|
+
declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
|
|
28
41
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
29
42
|
|
|
30
|
-
export { _default as default };
|
|
43
|
+
export { SERVICE_UPLOADS_ROUTE, _default as default };
|
|
31
44
|
export type { ChunkUploaderRoutesConfig, ModuleOptions };
|
package/dist/module.d.ts
CHANGED
|
@@ -24,8 +24,21 @@ interface ModuleOptions {
|
|
|
24
24
|
final?: boolean;
|
|
25
25
|
};
|
|
26
26
|
headers?: Record<string, string> | (() => Record<string, string>);
|
|
27
|
+
/**
|
|
28
|
+
* Uploads started by the app's backend services (`useMLServiceUpload`).
|
|
29
|
+
* `proxy: true` adds `/api/media-library/service-uploads/**`, a same-origin
|
|
30
|
+
* pass-through that forwards the browser's media grant and nothing else.
|
|
31
|
+
* `baseURL` is where the browser sends those uploads (the media library's
|
|
32
|
+
* public API root with `/v1`, or a proxy route); with `proxy` it defaults
|
|
33
|
+
* to the pass-through route.
|
|
34
|
+
*/
|
|
35
|
+
serviceUploads?: {
|
|
36
|
+
proxy?: boolean;
|
|
37
|
+
baseURL?: string;
|
|
38
|
+
};
|
|
27
39
|
}
|
|
40
|
+
declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
|
|
28
41
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
29
42
|
|
|
30
|
-
export { _default as default };
|
|
43
|
+
export { SERVICE_UPLOADS_ROUTE, _default as default };
|
|
31
44
|
export type { ChunkUploaderRoutesConfig, ModuleOptions };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -4,6 +4,7 @@ export * from '../dist/runtime/types/uploader.js';
|
|
|
4
4
|
export * from '../dist/runtime/types/preview.js';
|
|
5
5
|
export * from '../dist/runtime/types/asset.js';
|
|
6
6
|
|
|
7
|
+
const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
|
|
7
8
|
const module$1 = defineNuxtModule({
|
|
8
9
|
meta: {
|
|
9
10
|
name: "media-library",
|
|
@@ -89,6 +90,12 @@ const module$1 = defineNuxtModule({
|
|
|
89
90
|
route: "/api/media-library/manage/processing/**",
|
|
90
91
|
handler: resolver.resolve("./runtime/server/processing-proxy")
|
|
91
92
|
});
|
|
93
|
+
if (options.serviceUploads?.proxy) {
|
|
94
|
+
addServerHandler({
|
|
95
|
+
route: `${SERVICE_UPLOADS_ROUTE}/**`,
|
|
96
|
+
handler: resolver.resolve("./runtime/server/service-uploads-proxy")
|
|
97
|
+
});
|
|
98
|
+
}
|
|
92
99
|
nuxt.options.runtimeConfig.mediaLibrary = {
|
|
93
100
|
...existingPrivate,
|
|
94
101
|
baseURL: existingPrivate.baseURL ?? envBaseURL ?? options.baseURL ?? "",
|
|
@@ -110,7 +117,9 @@ const module$1 = defineNuxtModule({
|
|
|
110
117
|
perPart: existingPublic.checksum?.perPart ?? options.checksum?.perPart ?? true,
|
|
111
118
|
final: existingPublic.checksum?.final ?? options.checksum?.final ?? true
|
|
112
119
|
},
|
|
113
|
-
headers: existingPublic.headers ?? options.headers ?? {}
|
|
120
|
+
headers: existingPublic.headers ?? options.headers ?? {},
|
|
121
|
+
// Where useMLServiceUpload sends parts (NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BASE_URL).
|
|
122
|
+
serviceUploadBaseURL: existingPublic.serviceUploadBaseURL || options.serviceUploads?.baseURL || (options.serviceUploads?.proxy ? SERVICE_UPLOADS_ROUTE : "")
|
|
114
123
|
};
|
|
115
124
|
addPlugin({
|
|
116
125
|
src: resolver.resolve("./runtime/plugins/config")
|
|
@@ -118,4 +127,4 @@ const module$1 = defineNuxtModule({
|
|
|
118
127
|
}
|
|
119
128
|
});
|
|
120
129
|
|
|
121
|
-
export { module$1 as default };
|
|
130
|
+
export { SERVICE_UPLOADS_ROUTE, module$1 as default };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type RejectedFile } from '../../utils/fileAccept.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Browser accept syntax ("image/*,.glb"). Build one from kinds with acceptForKinds(). */
|
|
4
|
+
accept?: string;
|
|
5
|
+
multiple?: boolean;
|
|
6
|
+
/** Bytes. Larger files are rejected with reason "size". */
|
|
7
|
+
maxSize?: number | null;
|
|
8
|
+
/** How many more files fit. Extra files are rejected with reason "count". */
|
|
9
|
+
maxFiles?: number | null;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* `area`: a dashed box with an upload button (fields, picker).
|
|
13
|
+
* `overlay`: wraps the default slot and only shows a highlight while files are dragged over it (gallery).
|
|
14
|
+
*/
|
|
15
|
+
variant?: 'area' | 'overlay';
|
|
16
|
+
title?: string;
|
|
17
|
+
hint?: string;
|
|
18
|
+
/** The button that opens the computer's file dialog. Defaults to "Upload file(s)". */
|
|
19
|
+
buttonLabel?: string;
|
|
20
|
+
/** One short row instead of a box, for places where the drop zone is secondary (the picker). */
|
|
21
|
+
compact?: boolean;
|
|
22
|
+
};
|
|
23
|
+
type __VLS_Slots = {
|
|
24
|
+
default?: (props: {
|
|
25
|
+
isDragging: boolean;
|
|
26
|
+
open: () => void;
|
|
27
|
+
}) => unknown;
|
|
28
|
+
actions?: (props: {
|
|
29
|
+
open: () => void;
|
|
30
|
+
}) => unknown;
|
|
31
|
+
};
|
|
32
|
+
declare function open(): void;
|
|
33
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
34
|
+
open: typeof open;
|
|
35
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
36
|
+
files: (files: File[]) => any;
|
|
37
|
+
rejected: (files: RejectedFile[]) => any;
|
|
38
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
39
|
+
onFiles?: ((files: File[]) => any) | undefined;
|
|
40
|
+
onRejected?: ((files: RejectedFile[]) => any) | undefined;
|
|
41
|
+
}>, {
|
|
42
|
+
variant: "area" | "overlay";
|
|
43
|
+
compact: boolean;
|
|
44
|
+
disabled: boolean;
|
|
45
|
+
multiple: boolean;
|
|
46
|
+
title: string;
|
|
47
|
+
accept: string;
|
|
48
|
+
maxSize: number | null;
|
|
49
|
+
maxFiles: number | null;
|
|
50
|
+
hint: string;
|
|
51
|
+
buttonLabel: string;
|
|
52
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
53
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
54
|
+
declare const _default: typeof __VLS_export;
|
|
55
|
+
export default _default;
|
|
56
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
57
|
+
new (): {
|
|
58
|
+
$slots: S;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import { useFileUpload } from "#imports";
|
|
4
|
+
import { formatBytes } from "../../utils/assetFormat";
|
|
5
|
+
import { partitionFiles } from "../../utils/fileAccept";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
accept: { type: String, required: false, default: "*" },
|
|
8
|
+
multiple: { type: Boolean, required: false, default: true },
|
|
9
|
+
maxSize: { type: [Number, null], required: false, default: null },
|
|
10
|
+
maxFiles: { type: [Number, null], required: false, default: null },
|
|
11
|
+
disabled: { type: Boolean, required: false, default: false },
|
|
12
|
+
variant: { type: String, required: false, default: "area" },
|
|
13
|
+
title: { type: String, required: false, default: void 0 },
|
|
14
|
+
hint: { type: String, required: false, default: void 0 },
|
|
15
|
+
buttonLabel: { type: String, required: false, default: void 0 },
|
|
16
|
+
compact: { type: Boolean, required: false, default: false }
|
|
17
|
+
});
|
|
18
|
+
const emit = defineEmits(["files", "rejected"]);
|
|
19
|
+
defineSlots();
|
|
20
|
+
const { isDragging, dropzoneRef } = useFileUpload({
|
|
21
|
+
accept: "*",
|
|
22
|
+
multiple: true,
|
|
23
|
+
dropzone: true,
|
|
24
|
+
onUpdate: (files) => handle(files)
|
|
25
|
+
});
|
|
26
|
+
const input = ref();
|
|
27
|
+
function handle(files) {
|
|
28
|
+
if (props.disabled || !files.length) return;
|
|
29
|
+
const max = props.multiple ? props.maxFiles : Math.min(1, props.maxFiles ?? 1);
|
|
30
|
+
const { accepted, rejected } = partitionFiles(files, { accept: props.accept, maxSize: props.maxSize, maxFiles: max });
|
|
31
|
+
if (accepted.length) emit("files", accepted);
|
|
32
|
+
if (rejected.length) emit("rejected", rejected);
|
|
33
|
+
}
|
|
34
|
+
function open() {
|
|
35
|
+
if (!props.disabled) input.value?.click();
|
|
36
|
+
}
|
|
37
|
+
function onInput(e) {
|
|
38
|
+
const el = e.target;
|
|
39
|
+
handle(Array.from(el.files ?? []));
|
|
40
|
+
el.value = "";
|
|
41
|
+
}
|
|
42
|
+
const carriesFiles = ref(false);
|
|
43
|
+
function onDragEnter(e) {
|
|
44
|
+
carriesFiles.value = !!e.dataTransfer && Array.from(e.dataTransfer.types).includes("Files");
|
|
45
|
+
}
|
|
46
|
+
const active = computed(() => isDragging.value && carriesFiles.value && !props.disabled);
|
|
47
|
+
const sizeHint = computed(() => props.maxSize ? `Up to ${formatBytes(props.maxSize)}` : "");
|
|
48
|
+
defineExpose({ open });
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div
|
|
53
|
+
ref="dropzoneRef"
|
|
54
|
+
class="relative"
|
|
55
|
+
@dragenter.capture="onDragEnter"
|
|
56
|
+
:data-dragging="active || void 0"
|
|
57
|
+
:aria-disabled="disabled || void 0"
|
|
58
|
+
>
|
|
59
|
+
<input
|
|
60
|
+
ref="input"
|
|
61
|
+
type="file"
|
|
62
|
+
class="sr-only"
|
|
63
|
+
tabindex="-1"
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
:accept="accept === '*' ? void 0 : accept"
|
|
66
|
+
:multiple="multiple"
|
|
67
|
+
:disabled="disabled"
|
|
68
|
+
@change="onInput"
|
|
69
|
+
>
|
|
70
|
+
|
|
71
|
+
<template v-if="variant === 'overlay'">
|
|
72
|
+
<slot
|
|
73
|
+
:is-dragging="active"
|
|
74
|
+
:open="open"
|
|
75
|
+
/>
|
|
76
|
+
<div
|
|
77
|
+
v-if="active"
|
|
78
|
+
class="pointer-events-none absolute inset-0 z-20 grid place-items-center rounded-[inherit] border-2 border-dashed border-primary bg-primary/10 backdrop-blur-[1px]"
|
|
79
|
+
>
|
|
80
|
+
<span class="flex items-center gap-2 rounded-md bg-default px-3 py-1.5 text-sm font-semibold text-primary shadow-sm">
|
|
81
|
+
<UIcon
|
|
82
|
+
name="i-lucide-upload"
|
|
83
|
+
class="size-4"
|
|
84
|
+
/>
|
|
85
|
+
{{ title ?? "Drop to upload" }}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<div
|
|
91
|
+
v-else
|
|
92
|
+
class="flex items-center rounded-lg border-[1.5px] border-dashed text-center transition-colors motion-reduce:transition-none"
|
|
93
|
+
:class="[
|
|
94
|
+
compact ? 'flex-row flex-wrap justify-center gap-x-3 gap-y-1.5 px-3 py-2.5' : 'flex-col gap-2.5 px-4 py-5',
|
|
95
|
+
active ? 'border-primary bg-primary/10' : 'border-accented bg-default',
|
|
96
|
+
disabled ? 'opacity-60' : ''
|
|
97
|
+
]"
|
|
98
|
+
>
|
|
99
|
+
<slot
|
|
100
|
+
:is-dragging="active"
|
|
101
|
+
:open="open"
|
|
102
|
+
>
|
|
103
|
+
<UIcon
|
|
104
|
+
v-if="!compact"
|
|
105
|
+
name="i-lucide-upload"
|
|
106
|
+
class="size-5"
|
|
107
|
+
:class="active ? 'text-primary' : 'text-dimmed'"
|
|
108
|
+
/>
|
|
109
|
+
<p class="text-sm">
|
|
110
|
+
<span :class="compact ? 'text-muted' : 'font-semibold'">{{ title ?? (multiple ? "Drop files here" : "Drop a file here") }}</span>
|
|
111
|
+
<span class="text-muted"> or</span>
|
|
112
|
+
</p>
|
|
113
|
+
<div class="flex flex-wrap justify-center gap-1.5">
|
|
114
|
+
<UButton
|
|
115
|
+
size="sm"
|
|
116
|
+
color="neutral"
|
|
117
|
+
variant="outline"
|
|
118
|
+
icon="i-lucide-upload"
|
|
119
|
+
:label="buttonLabel ?? (multiple ? 'Upload files' : 'Upload file')"
|
|
120
|
+
:disabled="disabled"
|
|
121
|
+
@click="open"
|
|
122
|
+
/>
|
|
123
|
+
<slot
|
|
124
|
+
name="actions"
|
|
125
|
+
:open="open"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<p
|
|
129
|
+
v-if="hint || sizeHint"
|
|
130
|
+
class="text-xs text-dimmed"
|
|
131
|
+
>
|
|
132
|
+
{{ [hint, sizeHint].filter(Boolean).join(" \xB7 ") }}
|
|
133
|
+
</p>
|
|
134
|
+
</slot>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type RejectedFile } from '../../utils/fileAccept.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Browser accept syntax ("image/*,.glb"). Build one from kinds with acceptForKinds(). */
|
|
4
|
+
accept?: string;
|
|
5
|
+
multiple?: boolean;
|
|
6
|
+
/** Bytes. Larger files are rejected with reason "size". */
|
|
7
|
+
maxSize?: number | null;
|
|
8
|
+
/** How many more files fit. Extra files are rejected with reason "count". */
|
|
9
|
+
maxFiles?: number | null;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* `area`: a dashed box with an upload button (fields, picker).
|
|
13
|
+
* `overlay`: wraps the default slot and only shows a highlight while files are dragged over it (gallery).
|
|
14
|
+
*/
|
|
15
|
+
variant?: 'area' | 'overlay';
|
|
16
|
+
title?: string;
|
|
17
|
+
hint?: string;
|
|
18
|
+
/** The button that opens the computer's file dialog. Defaults to "Upload file(s)". */
|
|
19
|
+
buttonLabel?: string;
|
|
20
|
+
/** One short row instead of a box, for places where the drop zone is secondary (the picker). */
|
|
21
|
+
compact?: boolean;
|
|
22
|
+
};
|
|
23
|
+
type __VLS_Slots = {
|
|
24
|
+
default?: (props: {
|
|
25
|
+
isDragging: boolean;
|
|
26
|
+
open: () => void;
|
|
27
|
+
}) => unknown;
|
|
28
|
+
actions?: (props: {
|
|
29
|
+
open: () => void;
|
|
30
|
+
}) => unknown;
|
|
31
|
+
};
|
|
32
|
+
declare function open(): void;
|
|
33
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
34
|
+
open: typeof open;
|
|
35
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
36
|
+
files: (files: File[]) => any;
|
|
37
|
+
rejected: (files: RejectedFile[]) => any;
|
|
38
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
39
|
+
onFiles?: ((files: File[]) => any) | undefined;
|
|
40
|
+
onRejected?: ((files: RejectedFile[]) => any) | undefined;
|
|
41
|
+
}>, {
|
|
42
|
+
variant: "area" | "overlay";
|
|
43
|
+
compact: boolean;
|
|
44
|
+
disabled: boolean;
|
|
45
|
+
multiple: boolean;
|
|
46
|
+
title: string;
|
|
47
|
+
accept: string;
|
|
48
|
+
maxSize: number | null;
|
|
49
|
+
maxFiles: number | null;
|
|
50
|
+
hint: string;
|
|
51
|
+
buttonLabel: string;
|
|
52
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
53
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
54
|
+
declare const _default: typeof __VLS_export;
|
|
55
|
+
export default _default;
|
|
56
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
57
|
+
new (): {
|
|
58
|
+
$slots: S;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MediaUsage } from './UsageList.vue.js';
|
|
2
|
+
export type MediaEditorResult = {
|
|
3
|
+
action: 'save';
|
|
4
|
+
alt: Record<string, string>;
|
|
5
|
+
} | {
|
|
6
|
+
action: 'remove';
|
|
7
|
+
assetId: string;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_Props = {
|
|
10
|
+
assetId: string;
|
|
11
|
+
siblings?: readonly string[];
|
|
12
|
+
alt?: Readonly<Record<string, string>>;
|
|
13
|
+
readonly?: boolean;
|
|
14
|
+
removeLabel?: string;
|
|
15
|
+
usages?: Readonly<Record<string, MediaUsage[]>>;
|
|
16
|
+
};
|
|
17
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
|
+
close: (result: MediaEditorResult | null) => any;
|
|
19
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
20
|
+
onClose?: ((result: MediaEditorResult | null) => any) | undefined;
|
|
21
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
22
|
+
declare const _default: typeof __VLS_export;
|
|
23
|
+
export default _default;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
assetId: { type: String, required: true },
|
|
4
|
+
siblings: { type: Array, required: false },
|
|
5
|
+
alt: { type: Object, required: false },
|
|
6
|
+
readonly: { type: Boolean, required: false },
|
|
7
|
+
removeLabel: { type: String, required: false },
|
|
8
|
+
usages: { type: Object, required: false }
|
|
9
|
+
});
|
|
10
|
+
const emit = defineEmits(["close"]);
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<UModal
|
|
15
|
+
:title="readonly ? 'Preview' : 'Edit media'"
|
|
16
|
+
:close="{ onClick: () => emit('close', null) }"
|
|
17
|
+
:ui="{ content: 'sm:max-w-5xl', body: 'p-0 sm:p-0' }"
|
|
18
|
+
@update:open="(open) => !open && emit('close', null)"
|
|
19
|
+
>
|
|
20
|
+
<template #body>
|
|
21
|
+
<MLMediaEditorPanel
|
|
22
|
+
:asset-id="assetId"
|
|
23
|
+
:siblings="siblings"
|
|
24
|
+
:alt="alt"
|
|
25
|
+
:readonly="readonly"
|
|
26
|
+
:remove-label="removeLabel"
|
|
27
|
+
:usages="usages"
|
|
28
|
+
@save="({ alt: a }) => emit('close', { action: 'save', alt: a })"
|
|
29
|
+
@remove="(id) => emit('close', { action: 'remove', assetId: id })"
|
|
30
|
+
@cancel="emit('close', null)"
|
|
31
|
+
/>
|
|
32
|
+
</template>
|
|
33
|
+
</UModal>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MediaUsage } from './UsageList.vue.js';
|
|
2
|
+
export type MediaEditorResult = {
|
|
3
|
+
action: 'save';
|
|
4
|
+
alt: Record<string, string>;
|
|
5
|
+
} | {
|
|
6
|
+
action: 'remove';
|
|
7
|
+
assetId: string;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_Props = {
|
|
10
|
+
assetId: string;
|
|
11
|
+
siblings?: readonly string[];
|
|
12
|
+
alt?: Readonly<Record<string, string>>;
|
|
13
|
+
readonly?: boolean;
|
|
14
|
+
removeLabel?: string;
|
|
15
|
+
usages?: Readonly<Record<string, MediaUsage[]>>;
|
|
16
|
+
};
|
|
17
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
|
+
close: (result: MediaEditorResult | null) => any;
|
|
19
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
20
|
+
onClose?: ((result: MediaEditorResult | null) => any) | undefined;
|
|
21
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
22
|
+
declare const _default: typeof __VLS_export;
|
|
23
|
+
export default _default;
|
|
@@ -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;
|