nuxt-media-library 0.0.48 → 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/dist/module.json +1 -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/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/index.d.ts +11 -0
- package/dist/runtime/index.js +11 -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/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { useMLMediaAssets } from "../../composables/useMLMediaAssets";
|
|
4
|
+
import { useMLMediaBrowse } from "../../composables/useMLMediaBrowse";
|
|
5
|
+
import { useMLMediaUpload } from "../../composables/useMLMediaUpload";
|
|
6
|
+
import { useMLMediaEditor } from "../../composables/useMLMediaEditor";
|
|
7
|
+
import { assetKindMeta } from "../../utils/assetDisplay";
|
|
8
|
+
import { acceptForKinds, describeKinds } from "../../utils/fileAccept";
|
|
9
|
+
import { formatBytes } from "../../utils/assetFormat";
|
|
10
|
+
const ALL_KINDS = ["image", "video", "audio", "document", "model_3d", "archive", "font", "web_app", "image_batch", "other"];
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
accept: { type: [Array, String], required: false, default: "" },
|
|
13
|
+
max: { type: Number, required: false, default: Number.POSITIVE_INFINITY },
|
|
14
|
+
selected: { type: Array, required: false, default: () => [] },
|
|
15
|
+
title: { type: String, required: false, default: void 0 },
|
|
16
|
+
maxSize: { type: [Number, null], required: false, default: null },
|
|
17
|
+
upload: { type: Object, required: false, default: () => ({}) },
|
|
18
|
+
bodyClass: { type: String, required: false, default: "max-h-[min(60vh,560px)]" }
|
|
19
|
+
});
|
|
20
|
+
const emit = defineEmits(["done", "cancel"]);
|
|
21
|
+
const acceptKinds = computed(() => {
|
|
22
|
+
const raw = typeof props.accept === "string" ? props.accept.split(",") : [...props.accept];
|
|
23
|
+
return raw.map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
24
|
+
});
|
|
25
|
+
const isAccepted = (kind) => !acceptKinds.value.length || acceptKinds.value.includes((kind ?? "").toLowerCase());
|
|
26
|
+
const notAcceptedReason = computed(() => `Only ${describeKinds(acceptKinds.value)} can be chosen here`);
|
|
27
|
+
const search = ref("");
|
|
28
|
+
const view = ref("grid");
|
|
29
|
+
const kindFilter = ref(acceptKinds.value.length === 1 ? [acceptKinds.value[0]] : []);
|
|
30
|
+
const kindOptions = computed(
|
|
31
|
+
() => (acceptKinds.value.length ? acceptKinds.value : ALL_KINDS).map((k) => ({ label: assetKindMeta(k).label, value: k }))
|
|
32
|
+
);
|
|
33
|
+
const filtering = computed(() => !!search.value.trim() || kindFilter.value.length > 0);
|
|
34
|
+
const browse = useMLMediaBrowse(() => ({ kind: kindFilter.value[0] ?? null, query: search.value }));
|
|
35
|
+
const initial = [...props.selected];
|
|
36
|
+
const chosen = ref([...initial]);
|
|
37
|
+
const limitNote = ref(null);
|
|
38
|
+
const single = computed(() => props.max === 1);
|
|
39
|
+
const changed = computed(() => chosen.value.length !== initial.length || chosen.value.some((id, i) => id !== initial[i]));
|
|
40
|
+
function isChosen(id) {
|
|
41
|
+
return !!id && chosen.value.includes(id);
|
|
42
|
+
}
|
|
43
|
+
function choose(id) {
|
|
44
|
+
limitNote.value = null;
|
|
45
|
+
if (single.value) {
|
|
46
|
+
chosen.value = [id];
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (chosen.value.length >= props.max) {
|
|
50
|
+
limitNote.value = `You can choose up to ${props.max}. Unselect one first.`;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
chosen.value = [...chosen.value, id];
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function toggle(id) {
|
|
57
|
+
if (isChosen(id)) {
|
|
58
|
+
limitNote.value = null;
|
|
59
|
+
chosen.value = chosen.value.filter((x) => x !== id);
|
|
60
|
+
} else {
|
|
61
|
+
choose(id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const pinned = useMLMediaAssets(() => initial);
|
|
65
|
+
const pinnedAssets = computed(() => initial.map((id) => pinned.get(id)).filter((a) => !!a));
|
|
66
|
+
const uploader = useMLMediaUpload(props.upload);
|
|
67
|
+
const rejectNote = ref(null);
|
|
68
|
+
function onFiles(files) {
|
|
69
|
+
rejectNote.value = null;
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const u = uploader.start(file);
|
|
72
|
+
void u.created.then((id) => id && !isChosen(id) && choose(id));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function onRejected(rejected) {
|
|
76
|
+
const first = rejected[0];
|
|
77
|
+
if (!first) return;
|
|
78
|
+
const more = rejected.length > 1 ? ` (and ${rejected.length - 1} more)` : "";
|
|
79
|
+
rejectNote.value = first.reason === "type" ? `${first.file.name}${more} can't be used here. Only ${describeKinds(acceptKinds.value)}.` : `${first.file.name}${more} is larger than ${props.maxSize ? formatBytes(props.maxSize) : "the limit"}.`;
|
|
80
|
+
}
|
|
81
|
+
const uploadIds = computed(() => new Set(uploader.uploads.value.map((u) => u.assetId).filter(Boolean)));
|
|
82
|
+
const visibleUploads = computed(() => uploader.uploads.value.filter((u) => u.phase !== "canceled"));
|
|
83
|
+
function uploadPhase(u) {
|
|
84
|
+
return u.phase === "uploading" || u.phase === "processing" || u.phase === "failed" ? u.phase : null;
|
|
85
|
+
}
|
|
86
|
+
const listed = computed(() => {
|
|
87
|
+
const pinnedIds = new Set(filtering.value ? [] : pinnedAssets.value.map((a) => a.id));
|
|
88
|
+
const rest = browse.items.value.filter((a) => !pinnedIds.has(a.id) && !uploadIds.value.has(a.id));
|
|
89
|
+
return filtering.value ? rest : [...pinnedAssets.value, ...rest];
|
|
90
|
+
});
|
|
91
|
+
const initialLoading = computed(() => browse.loading.value && !browse.items.value.length);
|
|
92
|
+
const isEmpty = computed(() => !initialLoading.value && !listed.value.length && !visibleUploads.value.length);
|
|
93
|
+
watch(kindFilter, () => {
|
|
94
|
+
limitNote.value = null;
|
|
95
|
+
});
|
|
96
|
+
const mediaEditor = useMLMediaEditor();
|
|
97
|
+
function preview(id) {
|
|
98
|
+
void mediaEditor.open(id, { readonly: true, siblings: listed.value.map((a) => a.id) });
|
|
99
|
+
}
|
|
100
|
+
const footerText = computed(() => {
|
|
101
|
+
if (limitNote.value) return limitNote.value;
|
|
102
|
+
const n = chosen.value.length;
|
|
103
|
+
if (single.value) return n ? "1 selected" : "Nothing selected";
|
|
104
|
+
return Number.isFinite(props.max) ? `${n} of ${props.max} selected` : `${n} selected`;
|
|
105
|
+
});
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<template>
|
|
109
|
+
<div class="flex min-h-0 flex-col">
|
|
110
|
+
<div class="flex flex-col gap-3 border-b border-default p-4">
|
|
111
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
112
|
+
<UInput
|
|
113
|
+
v-model="search"
|
|
114
|
+
icon="i-lucide-search"
|
|
115
|
+
placeholder="Search files"
|
|
116
|
+
class="min-w-0 flex-1 basis-56"
|
|
117
|
+
:loading="browse.loading.value && !!search"
|
|
118
|
+
aria-label="Search files"
|
|
119
|
+
/>
|
|
120
|
+
<UFieldGroup size="md">
|
|
121
|
+
<UButton
|
|
122
|
+
icon="i-lucide-layout-grid"
|
|
123
|
+
color="neutral"
|
|
124
|
+
:variant="view === 'grid' ? 'soft' : 'outline'"
|
|
125
|
+
aria-label="Grid view"
|
|
126
|
+
:aria-pressed="view === 'grid'"
|
|
127
|
+
@click="view = 'grid'"
|
|
128
|
+
/>
|
|
129
|
+
<UButton
|
|
130
|
+
icon="i-lucide-list"
|
|
131
|
+
color="neutral"
|
|
132
|
+
:variant="view === 'list' ? 'soft' : 'outline'"
|
|
133
|
+
aria-label="List view"
|
|
134
|
+
:aria-pressed="view === 'list'"
|
|
135
|
+
@click="view = 'list'"
|
|
136
|
+
/>
|
|
137
|
+
</UFieldGroup>
|
|
138
|
+
</div>
|
|
139
|
+
<div
|
|
140
|
+
v-if="kindOptions.length > 1"
|
|
141
|
+
class="flex flex-wrap gap-1.5"
|
|
142
|
+
>
|
|
143
|
+
<MLMediaFilterPill
|
|
144
|
+
v-model="kindFilter"
|
|
145
|
+
label="File type"
|
|
146
|
+
:options="kindOptions"
|
|
147
|
+
single
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div
|
|
153
|
+
class="flex min-h-0 flex-col gap-3 overflow-y-auto p-4"
|
|
154
|
+
:class="bodyClass"
|
|
155
|
+
>
|
|
156
|
+
<MLMediaDropzone
|
|
157
|
+
:accept="acceptForKinds(acceptKinds)"
|
|
158
|
+
:multiple="!single"
|
|
159
|
+
:max-size="maxSize"
|
|
160
|
+
compact
|
|
161
|
+
:title="single ? 'Drop a file to add it to the library' : 'Drop files to add them to the library'"
|
|
162
|
+
button-label="Upload from computer"
|
|
163
|
+
@files="onFiles"
|
|
164
|
+
@rejected="onRejected"
|
|
165
|
+
/>
|
|
166
|
+
<p
|
|
167
|
+
v-if="rejectNote"
|
|
168
|
+
class="text-xs text-error"
|
|
169
|
+
>
|
|
170
|
+
{{ rejectNote }}
|
|
171
|
+
</p>
|
|
172
|
+
|
|
173
|
+
<UAlert
|
|
174
|
+
v-if="browse.error.value"
|
|
175
|
+
color="error"
|
|
176
|
+
variant="subtle"
|
|
177
|
+
icon="i-lucide-circle-alert"
|
|
178
|
+
:title="browse.error.value"
|
|
179
|
+
:actions="[{ label: 'Try again', color: 'neutral', variant: 'outline', onClick: () => browse.reload() }]"
|
|
180
|
+
/>
|
|
181
|
+
|
|
182
|
+
<!-- Loading -->
|
|
183
|
+
<div
|
|
184
|
+
v-if="initialLoading"
|
|
185
|
+
class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
|
|
186
|
+
>
|
|
187
|
+
<div
|
|
188
|
+
v-for="i in 12"
|
|
189
|
+
:key="i"
|
|
190
|
+
class="flex flex-col items-center gap-2 p-2.5"
|
|
191
|
+
>
|
|
192
|
+
<USkeleton class="aspect-square w-full max-w-28 rounded-md" />
|
|
193
|
+
<USkeleton class="h-3 w-3/4" />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<!-- Empty -->
|
|
198
|
+
<UEmpty
|
|
199
|
+
v-else-if="isEmpty && !browse.error.value"
|
|
200
|
+
variant="naked"
|
|
201
|
+
:icon="filtering ? 'i-lucide-search-x' : 'i-lucide-folder-open'"
|
|
202
|
+
:title="filtering ? 'No files match' : 'No files yet'"
|
|
203
|
+
:description="filtering ? 'Try another search or clear the file type.' : 'Drop files above to add the first one.'"
|
|
204
|
+
/>
|
|
205
|
+
|
|
206
|
+
<!-- Grid -->
|
|
207
|
+
<div
|
|
208
|
+
v-else-if="view === 'grid'"
|
|
209
|
+
class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
|
|
210
|
+
>
|
|
211
|
+
<MLMediaPickerCard
|
|
212
|
+
v-for="u in visibleUploads"
|
|
213
|
+
:key="u.key"
|
|
214
|
+
:src="u.localPreview"
|
|
215
|
+
:kind="u.kind"
|
|
216
|
+
:name="u.file.name"
|
|
217
|
+
:meta="u.phase === 'failed' ? u.error ?? 'Failed' : void 0"
|
|
218
|
+
:selected="isChosen(u.assetId)"
|
|
219
|
+
:disabled="!u.assetId || !isAccepted(u.kind)"
|
|
220
|
+
@toggle="u.assetId && toggle(u.assetId)"
|
|
221
|
+
>
|
|
222
|
+
<MLMediaProgressVeil
|
|
223
|
+
v-if="uploadPhase(u)"
|
|
224
|
+
:phase="uploadPhase(u)"
|
|
225
|
+
:progress="u.progress"
|
|
226
|
+
compact
|
|
227
|
+
/>
|
|
228
|
+
</MLMediaPickerCard>
|
|
229
|
+
<MLMediaPickerCard
|
|
230
|
+
v-for="a in listed"
|
|
231
|
+
:key="a.id"
|
|
232
|
+
:asset="a"
|
|
233
|
+
:selected="isChosen(a.id)"
|
|
234
|
+
:disabled="!isAccepted(a.kind) || a.status === 'failed'"
|
|
235
|
+
:disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
|
|
236
|
+
:on-preview="() => preview(a.id)"
|
|
237
|
+
@toggle="toggle(a.id)"
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<!-- List -->
|
|
242
|
+
<div
|
|
243
|
+
v-else
|
|
244
|
+
class="flex flex-col divide-y divide-default overflow-hidden rounded-lg border border-default"
|
|
245
|
+
>
|
|
246
|
+
<MLMediaPickerRow
|
|
247
|
+
v-for="u in visibleUploads"
|
|
248
|
+
:key="u.key"
|
|
249
|
+
:src="u.localPreview"
|
|
250
|
+
:kind="u.kind"
|
|
251
|
+
:name="u.file.name"
|
|
252
|
+
:size="u.phase === 'uploading' ? `${Math.round(u.progress ?? 0)}%` : formatBytes(u.file.size)"
|
|
253
|
+
:date="u.phase === 'ready' ? 'Just now' : u.phase === 'failed' ? 'Failed' : 'Uploading'"
|
|
254
|
+
:selected="isChosen(u.assetId)"
|
|
255
|
+
:disabled="!u.assetId || !isAccepted(u.kind)"
|
|
256
|
+
@toggle="u.assetId && toggle(u.assetId)"
|
|
257
|
+
/>
|
|
258
|
+
<MLMediaPickerRow
|
|
259
|
+
v-for="a in listed"
|
|
260
|
+
:key="a.id"
|
|
261
|
+
:asset="a"
|
|
262
|
+
:selected="isChosen(a.id)"
|
|
263
|
+
:disabled="!isAccepted(a.kind) || a.status === 'failed'"
|
|
264
|
+
:disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
|
|
265
|
+
@toggle="toggle(a.id)"
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<UButton
|
|
270
|
+
v-if="browse.hasMore.value && !initialLoading"
|
|
271
|
+
class="self-center"
|
|
272
|
+
color="neutral"
|
|
273
|
+
variant="outline"
|
|
274
|
+
size="sm"
|
|
275
|
+
label="Load more"
|
|
276
|
+
:loading="browse.loading.value"
|
|
277
|
+
@click="browse.loadMore()"
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="flex flex-wrap items-center gap-2 border-t border-default px-4 py-3">
|
|
282
|
+
<span
|
|
283
|
+
class="me-auto text-sm"
|
|
284
|
+
:class="limitNote ? 'text-warning' : 'text-muted'"
|
|
285
|
+
aria-live="polite"
|
|
286
|
+
>{{ footerText }}</span>
|
|
287
|
+
<UButton
|
|
288
|
+
color="neutral"
|
|
289
|
+
variant="outline"
|
|
290
|
+
label="Cancel"
|
|
291
|
+
@click="emit('cancel')"
|
|
292
|
+
/>
|
|
293
|
+
<UButton
|
|
294
|
+
label="Done"
|
|
295
|
+
:disabled="!changed"
|
|
296
|
+
@click="emit('done', [...chosen])"
|
|
297
|
+
/>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Kinds the caller takes. Others are listed but dimmed. Empty means any. */
|
|
4
|
+
accept?: readonly string[] | string;
|
|
5
|
+
/** Most files that can be chosen. 1 makes clicks replace the choice. */
|
|
6
|
+
max?: number;
|
|
7
|
+
/** IDs already attached. Listed first and checked. */
|
|
8
|
+
selected?: readonly string[];
|
|
9
|
+
title?: string;
|
|
10
|
+
/** Bytes, for uploads from the picker. */
|
|
11
|
+
maxSize?: number | null;
|
|
12
|
+
/** Upload session settings for files dropped into the picker. */
|
|
13
|
+
upload?: MediaUploadOptions;
|
|
14
|
+
/** Grid rows for a fixed-height body; the list scrolls inside it. */
|
|
15
|
+
bodyClass?: string;
|
|
16
|
+
};
|
|
17
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
|
+
cancel: () => any;
|
|
19
|
+
done: (ids: string[]) => any;
|
|
20
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
21
|
+
onCancel?: (() => any) | undefined;
|
|
22
|
+
onDone?: ((ids: string[]) => any) | undefined;
|
|
23
|
+
}>, {
|
|
24
|
+
title: string;
|
|
25
|
+
accept: readonly string[] | string;
|
|
26
|
+
maxSize: number | null;
|
|
27
|
+
max: number;
|
|
28
|
+
selected: readonly string[];
|
|
29
|
+
upload: MediaUploadOptions;
|
|
30
|
+
bodyClass: string;
|
|
31
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface FilterPillOption {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
/** Shown after the label, e.g. a count. */
|
|
5
|
+
hint?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
type __VLS_Props = {
|
|
9
|
+
/** "File type", "Used in"… */
|
|
10
|
+
label: string;
|
|
11
|
+
options: FilterPillOption[];
|
|
12
|
+
/** One option at a time (radio) instead of many (checkboxes). */
|
|
13
|
+
single?: boolean;
|
|
14
|
+
/** How many chosen labels to spell out before switching to "+N". */
|
|
15
|
+
maxShown?: number;
|
|
16
|
+
};
|
|
17
|
+
type __VLS_ModelProps = {
|
|
18
|
+
modelValue?: string[];
|
|
19
|
+
};
|
|
20
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
21
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
|
+
"update:modelValue": (value: string[]) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
24
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
25
|
+
}>, {
|
|
26
|
+
single: boolean;
|
|
27
|
+
maxShown: number;
|
|
28
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
|
+
declare const _default: typeof __VLS_export;
|
|
30
|
+
export default _default;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
label: { type: String, required: true },
|
|
5
|
+
options: { type: Array, required: true },
|
|
6
|
+
single: { type: Boolean, required: false, default: false },
|
|
7
|
+
maxShown: { type: Number, required: false, default: 2 }
|
|
8
|
+
});
|
|
9
|
+
const model = defineModel({ type: Array, ...{ default: () => [] } });
|
|
10
|
+
const chosen = computed(() => props.options.filter((o) => model.value.includes(o.value)));
|
|
11
|
+
const summary = computed(() => {
|
|
12
|
+
const names = chosen.value.map((o) => o.label);
|
|
13
|
+
if (names.length <= props.maxShown) return names.join(", ");
|
|
14
|
+
return `${names.slice(0, props.maxShown).join(", ")} +${names.length - props.maxShown}`;
|
|
15
|
+
});
|
|
16
|
+
const checkItems = computed(() => props.options.map((o) => ({
|
|
17
|
+
label: o.hint ? `${o.label} (${o.hint})` : o.label,
|
|
18
|
+
value: o.value,
|
|
19
|
+
disabled: o.disabled
|
|
20
|
+
})));
|
|
21
|
+
const singleValue = computed({
|
|
22
|
+
get: () => model.value[0],
|
|
23
|
+
set: (v) => {
|
|
24
|
+
model.value = v ? [v] : [];
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
function clear() {
|
|
28
|
+
model.value = [];
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<UFieldGroup size="sm">
|
|
34
|
+
<UPopover :content="{ align: 'start' }">
|
|
35
|
+
<UButton
|
|
36
|
+
color="neutral"
|
|
37
|
+
:variant="chosen.length ? 'soft' : 'outline'"
|
|
38
|
+
:trailing-icon="chosen.length ? void 0 : 'i-lucide-chevron-down'"
|
|
39
|
+
>
|
|
40
|
+
<span>{{ label }}<span
|
|
41
|
+
v-if="chosen.length"
|
|
42
|
+
class="font-normal"
|
|
43
|
+
>: {{ summary }}</span></span>
|
|
44
|
+
</UButton>
|
|
45
|
+
|
|
46
|
+
<template #content>
|
|
47
|
+
<div class="flex min-w-48 flex-col gap-2 p-3">
|
|
48
|
+
<URadioGroup
|
|
49
|
+
v-if="single"
|
|
50
|
+
v-model="singleValue"
|
|
51
|
+
:items="checkItems"
|
|
52
|
+
size="sm"
|
|
53
|
+
:legend="label"
|
|
54
|
+
:ui="{ legend: 'sr-only' }"
|
|
55
|
+
/>
|
|
56
|
+
<UCheckboxGroup
|
|
57
|
+
v-else
|
|
58
|
+
v-model="model"
|
|
59
|
+
:items="checkItems"
|
|
60
|
+
size="sm"
|
|
61
|
+
:legend="label"
|
|
62
|
+
:ui="{ legend: 'sr-only' }"
|
|
63
|
+
/>
|
|
64
|
+
<UButton
|
|
65
|
+
v-if="chosen.length"
|
|
66
|
+
size="xs"
|
|
67
|
+
variant="link"
|
|
68
|
+
class="self-start px-0"
|
|
69
|
+
label="Clear"
|
|
70
|
+
@click="clear"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
</UPopover>
|
|
75
|
+
<UButton
|
|
76
|
+
v-if="chosen.length"
|
|
77
|
+
color="neutral"
|
|
78
|
+
variant="soft"
|
|
79
|
+
icon="i-lucide-x"
|
|
80
|
+
:aria-label="`Clear ${label}`"
|
|
81
|
+
@click="clear"
|
|
82
|
+
/>
|
|
83
|
+
</UFieldGroup>
|
|
84
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface FilterPillOption {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
/** Shown after the label, e.g. a count. */
|
|
5
|
+
hint?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
type __VLS_Props = {
|
|
9
|
+
/** "File type", "Used in"… */
|
|
10
|
+
label: string;
|
|
11
|
+
options: FilterPillOption[];
|
|
12
|
+
/** One option at a time (radio) instead of many (checkboxes). */
|
|
13
|
+
single?: boolean;
|
|
14
|
+
/** How many chosen labels to spell out before switching to "+N". */
|
|
15
|
+
maxShown?: number;
|
|
16
|
+
};
|
|
17
|
+
type __VLS_ModelProps = {
|
|
18
|
+
modelValue?: string[];
|
|
19
|
+
};
|
|
20
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
21
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
|
+
"update:modelValue": (value: string[]) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
24
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
25
|
+
}>, {
|
|
26
|
+
single: boolean;
|
|
27
|
+
maxShown: number;
|
|
28
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
|
+
declare const _default: typeof __VLS_export;
|
|
30
|
+
export default _default;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
|
|
2
|
+
export interface MediaGalleryRole {
|
|
3
|
+
/** Shown on chips, badges and menus, e.g. "Cover". */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Upload profile name for files uploaded into this role, e.g. "product-cover". */
|
|
6
|
+
profile?: string | null;
|
|
7
|
+
/** Asset IDs already in this role, in order. */
|
|
8
|
+
selected?: string[];
|
|
9
|
+
/** Most files in this role. 1 makes it a one-file slot: a new file replaces the old. */
|
|
10
|
+
max?: number | null;
|
|
11
|
+
accept?: readonly string[] | string;
|
|
12
|
+
maxSize?: number | null;
|
|
13
|
+
}
|
|
14
|
+
export type MediaGalleryProfiles = Record<string, MediaGalleryRole>;
|
|
15
|
+
type __VLS_Props = {
|
|
16
|
+
/** Roles of this usage. Omit for a single list driven by v-model. */
|
|
17
|
+
profiles?: MediaGalleryProfiles | null;
|
|
18
|
+
/** Role new uploads and picked files go to. Defaults to the first role that holds more than one file. */
|
|
19
|
+
defaultRole?: string;
|
|
20
|
+
accept?: readonly string[] | string;
|
|
21
|
+
max?: number | null;
|
|
22
|
+
maxSize?: number | null;
|
|
23
|
+
title?: string;
|
|
24
|
+
hint?: string;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
featured?: boolean;
|
|
27
|
+
recordName?: string;
|
|
28
|
+
visibility?: MediaUploadOptions['visibility'];
|
|
29
|
+
processingConfig?: MediaUploadOptions['processingConfig'];
|
|
30
|
+
collectionName?: string | null;
|
|
31
|
+
/** Upload profile for the single list (with `profiles`, each role has its own). */
|
|
32
|
+
profile?: string | null;
|
|
33
|
+
picker?: boolean;
|
|
34
|
+
onPick?: () => void;
|
|
35
|
+
editor?: boolean;
|
|
36
|
+
onOpen?: (assetId: string) => void;
|
|
37
|
+
};
|
|
38
|
+
type __VLS_ModelProps = {
|
|
39
|
+
modelValue?: string[];
|
|
40
|
+
'alt'?: Record<string, string> | undefined;
|
|
41
|
+
};
|
|
42
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
43
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
44
|
+
uploaded: (assetId: string, role: string) => any;
|
|
45
|
+
"update:modelValue": (value: string[]) => any;
|
|
46
|
+
"update:alt": (value: Record<string, string> | undefined) => any;
|
|
47
|
+
"update:profiles": (profiles: MediaGalleryProfiles) => any;
|
|
48
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
49
|
+
onUploaded?: ((assetId: string, role: string) => any) | undefined;
|
|
50
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
51
|
+
"onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
|
|
52
|
+
"onUpdate:profiles"?: ((profiles: MediaGalleryProfiles) => any) | undefined;
|
|
53
|
+
}>, {
|
|
54
|
+
visibility: "private" | "restricted" | "public";
|
|
55
|
+
title: string;
|
|
56
|
+
profile: string | null;
|
|
57
|
+
accept: readonly string[] | string;
|
|
58
|
+
maxSize: number | null;
|
|
59
|
+
processingConfig: {
|
|
60
|
+
stepOptions?: Record<string, Record<string, unknown>> | null;
|
|
61
|
+
useFilenameAsAssetId?: boolean;
|
|
62
|
+
enableTranscription?: boolean;
|
|
63
|
+
};
|
|
64
|
+
collectionName: string | null;
|
|
65
|
+
disabled: boolean;
|
|
66
|
+
max: number | null;
|
|
67
|
+
hint: string;
|
|
68
|
+
picker: boolean;
|
|
69
|
+
onPick: () => void;
|
|
70
|
+
editor: boolean;
|
|
71
|
+
onOpen: (assetId: string) => void;
|
|
72
|
+
featured: boolean;
|
|
73
|
+
recordName: string;
|
|
74
|
+
profiles: MediaGalleryProfiles | null;
|
|
75
|
+
defaultRole: string;
|
|
76
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
77
|
+
declare const _default: typeof __VLS_export;
|
|
78
|
+
export default _default;
|