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.
- package/README.md +71 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +4 -1
- package/dist/runtime/components/MLAssetManager/Processing.vue +3 -1
- 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/components/MLUploader/Item.d.vue.ts +2 -2
- package/dist/runtime/components/MLUploader/Item.vue.d.ts +2 -2
- package/dist/runtime/composables/useMLAssetPreview.js +17 -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/useMLMediaGrants.d.ts +23 -0
- package/dist/runtime/composables/useMLMediaGrants.js +22 -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 +36 -0
- package/dist/runtime/composables/useMLServiceUpload.js +32 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.js +11 -0
- package/dist/runtime/server/create-ml-proxy.d.ts +16 -0
- package/dist/runtime/server/create-ml-proxy.js +138 -40
- package/dist/runtime/server/grants.d.ts +124 -0
- package/dist/runtime/server/grants.js +161 -0
- package/dist/runtime/server/index.d.ts +2 -0
- package/dist/runtime/server/index.js +7 -0
- package/dist/runtime/server/uploads-proxy.js +5 -2
- package/dist/runtime/types/asset.d.ts +3 -1
- 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/grants/listeners.d.ts +2 -0
- package/dist/runtime/utils/grants/listeners.js +8 -0
- package/dist/runtime/utils/grants/store.d.ts +26 -0
- package/dist/runtime/utils/grants/store.js +37 -0
- package/dist/runtime/utils/uploader/serviceUpload.d.ts +84 -0
- package/dist/runtime/utils/uploader/serviceUpload.js +109 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -82,6 +82,77 @@ If you use the uploader component in Nuxt, provide config via the Nuxt plugin
|
|
|
82
82
|
</template>
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Authorizing `/manage` with media grants
|
|
86
|
+
|
|
87
|
+
The module's server proxies forward `/api/media-library/manage/*` to the
|
|
88
|
+
media library. Without an `authorize` hook they forward the browser's
|
|
89
|
+
credentials (or your static headers) as before. With one, every `/manage`
|
|
90
|
+
request is checked by your rules and sent upstream with a short-lived
|
|
91
|
+
**media grant** scoped to exactly that request (server:
|
|
92
|
+
`docs/security/media-grants.md`). Your app never re-implements the
|
|
93
|
+
endpoints, and no long-lived credential reaches the media library from the
|
|
94
|
+
browser.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// server/api/media-library/[...].ts
|
|
98
|
+
import { createMLCatchAllProxy, specFromNeed } from '@fritill-team/nuxt-media-library/server'
|
|
99
|
+
|
|
100
|
+
const COVER = { kind: 'image', visibility: 'public', allowedMimeTypes: ['image/*'], maxSizeBytes: 5_000_000 }
|
|
101
|
+
|
|
102
|
+
export default createMLCatchAllProxy({
|
|
103
|
+
async authorize(event, need) {
|
|
104
|
+
const user = await requireUser(event) // your session
|
|
105
|
+
const subject = String(user.id)
|
|
106
|
+
if (need.scope === 'upload' && !need.sessionId) { // creating an upload
|
|
107
|
+
return user.canUpload ? { scopes: ['upload'], profile: COVER, subject } : false
|
|
108
|
+
}
|
|
109
|
+
if (need.taskId) return false // or look up the task's asset
|
|
110
|
+
if (need.scope === 'manage:*') return user.isAdmin // tenant-wide list, stream
|
|
111
|
+
return (need.assetIds ?? []).every(id => user.canEdit(id))
|
|
112
|
+
? { ...specFromNeed(need), subject }
|
|
113
|
+
: false
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- `need`: `{ scope, method, path, assetIds?, taskId?, sessionId?, upload? }`
|
|
119
|
+
(`upload` is the create body: MIME type, size, file name).
|
|
120
|
+
- Return `false` (403), `true` (the need as is), or a spec
|
|
121
|
+
`{ scopes, assetIds?, sessionIds?, profile?, subject?, ttlSeconds? }`.
|
|
122
|
+
Creating an upload needs a `profile`: kind, visibility and processing come
|
|
123
|
+
from it, not from the browser. `manage:*` is only accepted for requests
|
|
124
|
+
that need it.
|
|
125
|
+
- Grants are issued with the private runtime config
|
|
126
|
+
`mediaLibrary.serviceToken` (`NUXT_MEDIA_LIBRARY_SERVICE_TOKEN`), or pass
|
|
127
|
+
`issueGrant(spec)` to get them from your backend (e.g. the Python
|
|
128
|
+
client's `create_grant_router`). They are cached per spec until shortly
|
|
129
|
+
before they expire; an upload's parts, finalize and abort reuse the grant
|
|
130
|
+
that created the session.
|
|
131
|
+
- `/delivery` and the processing metadata routes are not affected.
|
|
132
|
+
|
|
133
|
+
### Showing assets of several services: read grants
|
|
134
|
+
|
|
135
|
+
Every link comes from the media library; each service decides only for its
|
|
136
|
+
own assets by returning a read grant (`mlg_…`, e.g. the Python client's
|
|
137
|
+
`client.grants.read(ids)`) with the data that references them. Hand the
|
|
138
|
+
grants to the module and every preview resolve sends them in one request:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const { addMediaGrants } = useMLMediaGrants()
|
|
142
|
+
|
|
143
|
+
const lesson = await $fetch('/api/lessons/42') // course service
|
|
144
|
+
const profile = await $fetch('/api/profile') // profile service
|
|
145
|
+
addMediaGrants(lesson.mediaGrant, profile.mediaGrant) // "mlg_…" or { grant, expiresAt }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- Live grants (up to 10, latest expiry first) go in `X-Media-Grants` on
|
|
149
|
+
`POST /api/media-library/delivery/assets/resolve`; the proxy forwards
|
|
150
|
+
them. Expired ones are dropped.
|
|
151
|
+
- Assets no grant lists come back `forbidden` from the server and are shown
|
|
152
|
+
as broken (`isBroken(id)`), as before; they are resolved again whenever
|
|
153
|
+
grants are added.
|
|
154
|
+
- Grants live in the browser only, never in server-side state.
|
|
155
|
+
|
|
85
156
|
### Vue installation (non-Nuxt)
|
|
86
157
|
|
|
87
158
|
```bash
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -93,7 +93,10 @@ const module$1 = defineNuxtModule({
|
|
|
93
93
|
...existingPrivate,
|
|
94
94
|
baseURL: existingPrivate.baseURL ?? envBaseURL ?? options.baseURL ?? "",
|
|
95
95
|
routes: existingPrivate.routes ?? options.routes ?? routes,
|
|
96
|
-
headers: resolvedHeaders
|
|
96
|
+
headers: resolvedHeaders,
|
|
97
|
+
// Only read when a proxy has `authorize` and no `issueGrant` (media
|
|
98
|
+
// grants). Declared so NUXT_MEDIA_LIBRARY_SERVICE_TOKEN applies.
|
|
99
|
+
serviceToken: existingPrivate.serviceToken ?? ""
|
|
97
100
|
};
|
|
98
101
|
nuxt.options.runtimeConfig.public.mediaLibrary = {
|
|
99
102
|
...existingPublic,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, onMounted, watch } from "vue";
|
|
3
3
|
import { useMLResourcePolicy } from "../../composables/resourcePolicy/useMLResourcePolicy";
|
|
4
|
+
import { mediaGrantHeaders } from "../../composables/useMLMediaGrants";
|
|
4
5
|
const props = defineProps({
|
|
5
6
|
assetId: { type: String, required: true },
|
|
6
7
|
manager: { type: Object, required: true }
|
|
@@ -34,7 +35,8 @@ async function resolveVideoUrl() {
|
|
|
34
35
|
}
|
|
35
36
|
try {
|
|
36
37
|
const result = await $fetch(
|
|
37
|
-
`/api/media-library/delivery/assets/${encodeURIComponent(props.assetId)}
|
|
38
|
+
`/api/media-library/delivery/assets/${encodeURIComponent(props.assetId)}`,
|
|
39
|
+
{ headers: mediaGrantHeaders() }
|
|
38
40
|
);
|
|
39
41
|
videoUrl.value = result.downloadUrl ?? null;
|
|
40
42
|
} catch {
|
|
@@ -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
|
+
title: string;
|
|
43
|
+
accept: string;
|
|
44
|
+
maxSize: number | null;
|
|
45
|
+
variant: "area" | "overlay";
|
|
46
|
+
compact: boolean;
|
|
47
|
+
disabled: boolean;
|
|
48
|
+
multiple: boolean;
|
|
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
|
+
title: string;
|
|
43
|
+
accept: string;
|
|
44
|
+
maxSize: number | null;
|
|
45
|
+
variant: "area" | "overlay";
|
|
46
|
+
compact: boolean;
|
|
47
|
+
disabled: boolean;
|
|
48
|
+
multiple: boolean;
|
|
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;
|