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,422 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, nextTick, ref, watch } from "vue";
|
|
3
|
+
import { useMLMediaGallery } from "../../composables/useMLMediaGallery";
|
|
4
|
+
import { useMLFilePicker } from "../../composables/useMLFilePicker";
|
|
5
|
+
import { useMLMediaEditor } from "../../composables/useMLMediaEditor";
|
|
6
|
+
import { assetName } from "../../utils/assetFormat";
|
|
7
|
+
import { assetThumbnailUrl } from "../../utils/assetThumbnail";
|
|
8
|
+
import { acceptForKinds, partitionFiles } from "../../utils/fileAccept";
|
|
9
|
+
import { GALLERY_TILE_TYPE } from "../../utils/galleryDrag";
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
accept: { type: [Array, String], required: false, default: "" },
|
|
12
|
+
max: { type: [Number, null], required: false, default: null },
|
|
13
|
+
maxSize: { type: [Number, null], required: false, default: null },
|
|
14
|
+
title: { type: String, required: false, default: "Media" },
|
|
15
|
+
hint: { type: String, required: false, default: void 0 },
|
|
16
|
+
disabled: { type: Boolean, required: false, default: false },
|
|
17
|
+
featured: { type: Boolean, required: false, default: true },
|
|
18
|
+
recordName: { type: String, required: false, default: "" },
|
|
19
|
+
visibility: { type: String, required: false, default: void 0 },
|
|
20
|
+
processingConfig: { type: Object, required: false, default: void 0 },
|
|
21
|
+
collectionName: { type: [String, null], required: false, default: null },
|
|
22
|
+
picker: { type: Boolean, required: false, default: true },
|
|
23
|
+
onPick: { type: Function, required: false, default: void 0 },
|
|
24
|
+
editor: { type: Boolean, required: false, default: true },
|
|
25
|
+
onOpen: { type: Function, required: false, default: void 0 },
|
|
26
|
+
profile: { type: [String, null], required: false, default: null },
|
|
27
|
+
removeLabel: { type: String, required: false, default: void 0 },
|
|
28
|
+
badges: { type: Function, required: false, default: void 0 },
|
|
29
|
+
tileMenu: { type: Function, required: false, default: void 0 },
|
|
30
|
+
highlight: { type: [Set, null], required: false, default: null },
|
|
31
|
+
onAdd: { type: Function, required: false, default: void 0 }
|
|
32
|
+
});
|
|
33
|
+
const model = defineModel({ type: Array, ...{ default: () => [] } });
|
|
34
|
+
const alt = defineModel("alt", { type: null, ...{ default: void 0 } });
|
|
35
|
+
const emit = defineEmits(["uploaded"]);
|
|
36
|
+
const gallery = useMLMediaGallery(model, {
|
|
37
|
+
accept: () => props.accept,
|
|
38
|
+
max: () => props.max,
|
|
39
|
+
maxSize: () => props.maxSize,
|
|
40
|
+
visibility: props.visibility,
|
|
41
|
+
processingConfig: props.processingConfig,
|
|
42
|
+
collectionName: props.collectionName,
|
|
43
|
+
profile: props.profile,
|
|
44
|
+
onUploaded: (id) => emit("uploaded", id),
|
|
45
|
+
onAdd: (id, tag) => props.onAdd?.(id, tag)
|
|
46
|
+
});
|
|
47
|
+
const { items, message, full, replaces } = gallery;
|
|
48
|
+
const removeLabel = computed(() => props.removeLabel ?? (props.recordName ? `Remove from ${props.recordName}` : "Remove"));
|
|
49
|
+
const showAddTile = computed(() => !props.disabled && !full.value && !(replaces.value && items.value.length > 0));
|
|
50
|
+
const countLabel = computed(() => {
|
|
51
|
+
const n = model.value.length;
|
|
52
|
+
return Number.isFinite(gallery.limit.value) ? `${n} of ${gallery.limit.value}` : String(n);
|
|
53
|
+
});
|
|
54
|
+
const helpText = computed(() => {
|
|
55
|
+
if (props.hint) return props.hint;
|
|
56
|
+
const parts = [];
|
|
57
|
+
if (!replaces.value) parts.push("Drag to reorder.");
|
|
58
|
+
if (props.featured && !replaces.value) parts.push("The first file is the featured one.");
|
|
59
|
+
if (gallery.acceptLabel.value !== "any file") parts.push(`Takes ${gallery.acceptLabel.value}.`);
|
|
60
|
+
return parts.join(" ");
|
|
61
|
+
});
|
|
62
|
+
const selected = ref(/* @__PURE__ */ new Set());
|
|
63
|
+
watch(model, (ids) => {
|
|
64
|
+
const keep = new Set(ids);
|
|
65
|
+
if ([...selected.value].some((id) => !keep.has(id))) selected.value = new Set([...selected.value].filter((id) => keep.has(id)));
|
|
66
|
+
});
|
|
67
|
+
function setSelected(id, on) {
|
|
68
|
+
const next = new Set(selected.value);
|
|
69
|
+
if (on) next.add(id);
|
|
70
|
+
else next.delete(id);
|
|
71
|
+
selected.value = next;
|
|
72
|
+
}
|
|
73
|
+
function removeSelected() {
|
|
74
|
+
gallery.remove([...selected.value]);
|
|
75
|
+
selected.value = /* @__PURE__ */ new Set();
|
|
76
|
+
}
|
|
77
|
+
function tilePhase(item) {
|
|
78
|
+
const p = item.upload?.phase;
|
|
79
|
+
if (p === "uploading" || p === "processing" || p === "failed") return p;
|
|
80
|
+
const s = item.asset?.status?.toLowerCase();
|
|
81
|
+
if (s === "processing" || s === "uploaded" || s === "pending") return "processing";
|
|
82
|
+
if (s === "failed") return "failed";
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function tileSrc(item) {
|
|
86
|
+
if (item.upload?.localPreview && !assetThumbnailUrl(item.asset)) return item.upload.localPreview;
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
function tileName(item) {
|
|
90
|
+
if (item.lookup === "not_found" || item.lookup === "forbidden") return "Unavailable file";
|
|
91
|
+
return item.upload?.file.name ?? assetName(item.asset);
|
|
92
|
+
}
|
|
93
|
+
function tileKind(item) {
|
|
94
|
+
return item.asset?.kind ?? item.upload?.kind ?? null;
|
|
95
|
+
}
|
|
96
|
+
function missingAlt(item) {
|
|
97
|
+
return alt.value !== void 0 && !!item.id && (tileKind(item) ?? "").toLowerCase() === "image" && !alt.value[item.id]?.trim();
|
|
98
|
+
}
|
|
99
|
+
function onRetry(item) {
|
|
100
|
+
item.upload?.retry();
|
|
101
|
+
}
|
|
102
|
+
function onCancel(item) {
|
|
103
|
+
if (!item.upload) return;
|
|
104
|
+
if (item.id) item.upload.cancel();
|
|
105
|
+
else gallery.discard(item.upload);
|
|
106
|
+
}
|
|
107
|
+
const filePicker = useMLFilePicker();
|
|
108
|
+
const mediaEditor = useMLMediaEditor();
|
|
109
|
+
const canPick = computed(() => !props.disabled && (!!props.onPick || props.picker));
|
|
110
|
+
async function pickExisting() {
|
|
111
|
+
if (props.onPick) return props.onPick();
|
|
112
|
+
const ids = await filePicker.open({
|
|
113
|
+
accept: props.accept,
|
|
114
|
+
max: gallery.limit.value,
|
|
115
|
+
selected: model.value,
|
|
116
|
+
title: `Select ${props.title.toLowerCase()}`,
|
|
117
|
+
maxSize: props.maxSize,
|
|
118
|
+
upload: { visibility: props.visibility, processingConfig: props.processingConfig, collectionName: props.collectionName, profile: props.profile }
|
|
119
|
+
});
|
|
120
|
+
if (ids) gallery.setFromPicker(ids);
|
|
121
|
+
}
|
|
122
|
+
function openFor(item) {
|
|
123
|
+
const id = item.id;
|
|
124
|
+
if (!id || !(props.onOpen || props.editor) || item.lookup !== "ok") return void 0;
|
|
125
|
+
return () => void openEditor(id);
|
|
126
|
+
}
|
|
127
|
+
async function openEditor(id) {
|
|
128
|
+
if (props.onOpen) return props.onOpen(id);
|
|
129
|
+
const result = await mediaEditor.open(id, {
|
|
130
|
+
siblings: model.value,
|
|
131
|
+
alt: alt.value ?? {},
|
|
132
|
+
readonly: props.disabled,
|
|
133
|
+
removeLabel: removeLabel.value
|
|
134
|
+
});
|
|
135
|
+
if (result?.action === "save") alt.value = { ...alt.value ?? {}, ...result.alt };
|
|
136
|
+
else if (result?.action === "remove") gallery.remove([result.assetId]);
|
|
137
|
+
}
|
|
138
|
+
const dragFrom = ref(null);
|
|
139
|
+
const dragOver = ref(null);
|
|
140
|
+
const announcement = ref("");
|
|
141
|
+
const tileEls = ref({});
|
|
142
|
+
const dropzone = ref(null);
|
|
143
|
+
function onDragStart(e, index) {
|
|
144
|
+
const id = items.value[index]?.id;
|
|
145
|
+
if (!e.dataTransfer || !id) return;
|
|
146
|
+
dragFrom.value = index;
|
|
147
|
+
e.dataTransfer.effectAllowed = "copyMove";
|
|
148
|
+
e.dataTransfer.setData(GALLERY_TILE_TYPE, id);
|
|
149
|
+
}
|
|
150
|
+
function onDragOver(e, index) {
|
|
151
|
+
if (dragFrom.value === null || !e.dataTransfer?.types.includes(GALLERY_TILE_TYPE)) return;
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
e.dataTransfer.dropEffect = "move";
|
|
154
|
+
dragOver.value = index;
|
|
155
|
+
}
|
|
156
|
+
function onDrop(e, index) {
|
|
157
|
+
if (dragFrom.value === null) return;
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
moveAndAnnounce(dragFrom.value, index);
|
|
160
|
+
onDragEnd();
|
|
161
|
+
}
|
|
162
|
+
function onDragEnd() {
|
|
163
|
+
dragFrom.value = null;
|
|
164
|
+
dragOver.value = null;
|
|
165
|
+
}
|
|
166
|
+
function menuFor(item) {
|
|
167
|
+
if (!item.id || !props.tileMenu) return [];
|
|
168
|
+
return props.tileMenu(item.id, tileKind(item));
|
|
169
|
+
}
|
|
170
|
+
function highlightClass(item) {
|
|
171
|
+
if (!props.highlight) return "";
|
|
172
|
+
return item.id && props.highlight.has(item.id) ? "outline-2 outline-offset-2 outline-primary" : "opacity-35";
|
|
173
|
+
}
|
|
174
|
+
function uploadFiles(files, opts = {}) {
|
|
175
|
+
if (props.disabled) return;
|
|
176
|
+
const { accepted, rejected } = partitionFiles(files, {
|
|
177
|
+
accept: acceptForKinds(opts.accept ?? props.accept),
|
|
178
|
+
maxSize: opts.maxSize ?? props.maxSize,
|
|
179
|
+
maxFiles: opts.maxFiles
|
|
180
|
+
});
|
|
181
|
+
if (rejected.length) gallery.onRejected(rejected);
|
|
182
|
+
if (accepted.length) gallery.onFiles(accepted, { profile: opts.profile, tag: opts.tag });
|
|
183
|
+
}
|
|
184
|
+
defineExpose({ uploadFiles });
|
|
185
|
+
function moveAndAnnounce(from, to) {
|
|
186
|
+
const id = model.value[from];
|
|
187
|
+
if (!id) return;
|
|
188
|
+
gallery.move(from, to);
|
|
189
|
+
const at = model.value.indexOf(id);
|
|
190
|
+
const item = items.value[at];
|
|
191
|
+
announcement.value = `${item ? tileName(item) : "File"} moved to position ${at + 1} of ${model.value.length}`;
|
|
192
|
+
}
|
|
193
|
+
async function onTileKey(e, index) {
|
|
194
|
+
if (!e.altKey || props.disabled || index >= model.value.length) return;
|
|
195
|
+
const step = e.key === "ArrowLeft" || e.key === "ArrowUp" ? -1 : e.key === "ArrowRight" || e.key === "ArrowDown" ? 1 : 0;
|
|
196
|
+
if (!step) return;
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
const key = items.value[index]?.key;
|
|
199
|
+
moveAndAnnounce(index, index + step);
|
|
200
|
+
await nextTick();
|
|
201
|
+
if (key) tileEls.value[key]?.querySelector('[role="button"]')?.focus();
|
|
202
|
+
}
|
|
203
|
+
</script>
|
|
204
|
+
|
|
205
|
+
<template>
|
|
206
|
+
<section
|
|
207
|
+
class="flex flex-col gap-3"
|
|
208
|
+
:aria-label="title"
|
|
209
|
+
>
|
|
210
|
+
<!-- Header -->
|
|
211
|
+
<div class="flex min-h-8 flex-wrap items-center gap-2">
|
|
212
|
+
<template v-if="selected.size">
|
|
213
|
+
<UCheckbox
|
|
214
|
+
:model-value="true"
|
|
215
|
+
aria-label="Clear selection"
|
|
216
|
+
@update:model-value="selected = /* @__PURE__ */ new Set()"
|
|
217
|
+
/>
|
|
218
|
+
<span class="me-auto text-sm font-semibold">{{ selected.size }} selected</span>
|
|
219
|
+
<UButton
|
|
220
|
+
size="sm"
|
|
221
|
+
color="error"
|
|
222
|
+
variant="soft"
|
|
223
|
+
icon="i-lucide-x"
|
|
224
|
+
:label="removeLabel"
|
|
225
|
+
@click="removeSelected"
|
|
226
|
+
/>
|
|
227
|
+
</template>
|
|
228
|
+
<template v-else>
|
|
229
|
+
<h4 class="text-sm font-semibold">
|
|
230
|
+
{{ title }}
|
|
231
|
+
</h4>
|
|
232
|
+
<span class="me-auto text-xs text-muted tabular-nums">{{ countLabel }}</span>
|
|
233
|
+
<UButton
|
|
234
|
+
v-if="model.length && !disabled && !full"
|
|
235
|
+
size="sm"
|
|
236
|
+
variant="ghost"
|
|
237
|
+
:icon="replaces ? 'i-lucide-replace' : 'i-lucide-upload'"
|
|
238
|
+
:label="replaces ? 'Replace' : 'Upload'"
|
|
239
|
+
@click="dropzone?.open()"
|
|
240
|
+
/>
|
|
241
|
+
<UButton
|
|
242
|
+
v-if="canPick && model.length"
|
|
243
|
+
size="sm"
|
|
244
|
+
variant="ghost"
|
|
245
|
+
icon="i-lucide-layout-grid"
|
|
246
|
+
label="Select existing"
|
|
247
|
+
@click="pickExisting"
|
|
248
|
+
/>
|
|
249
|
+
</template>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<slot name="toolbar" />
|
|
253
|
+
|
|
254
|
+
<MLMediaDropzone
|
|
255
|
+
ref="dropzone"
|
|
256
|
+
variant="overlay"
|
|
257
|
+
:accept="gallery.acceptAttr.value"
|
|
258
|
+
multiple
|
|
259
|
+
:max-files="gallery.remaining.value"
|
|
260
|
+
:max-size="maxSize"
|
|
261
|
+
:disabled="disabled || full"
|
|
262
|
+
title="Drop to add"
|
|
263
|
+
class="rounded-lg"
|
|
264
|
+
@files="gallery.onFiles"
|
|
265
|
+
@rejected="gallery.onRejected"
|
|
266
|
+
>
|
|
267
|
+
<template #default="{ open }">
|
|
268
|
+
<!-- Empty -->
|
|
269
|
+
<div
|
|
270
|
+
v-if="!items.length"
|
|
271
|
+
class="flex flex-col items-center gap-2.5 rounded-lg border-[1.5px] border-dashed border-accented px-4 py-8 text-center"
|
|
272
|
+
>
|
|
273
|
+
<UIcon
|
|
274
|
+
name="i-lucide-images"
|
|
275
|
+
class="size-6 text-dimmed"
|
|
276
|
+
/>
|
|
277
|
+
<p class="text-sm">
|
|
278
|
+
<span class="font-semibold">{{ replaces ? "Drop a file here" : "Drop files here" }}</span>
|
|
279
|
+
<span class="text-muted"> or</span>
|
|
280
|
+
</p>
|
|
281
|
+
<div class="flex flex-wrap justify-center gap-1.5">
|
|
282
|
+
<UButton
|
|
283
|
+
size="sm"
|
|
284
|
+
color="neutral"
|
|
285
|
+
variant="outline"
|
|
286
|
+
icon="i-lucide-upload"
|
|
287
|
+
:label="replaces ? 'Upload file' : 'Upload files'"
|
|
288
|
+
:disabled="disabled"
|
|
289
|
+
@click="open"
|
|
290
|
+
/>
|
|
291
|
+
<UButton
|
|
292
|
+
v-if="canPick"
|
|
293
|
+
size="sm"
|
|
294
|
+
color="neutral"
|
|
295
|
+
variant="outline"
|
|
296
|
+
icon="i-lucide-layout-grid"
|
|
297
|
+
label="Select existing"
|
|
298
|
+
@click="pickExisting"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<!-- Grid -->
|
|
304
|
+
<ul
|
|
305
|
+
v-else
|
|
306
|
+
class="grid grid-cols-3 gap-2.5 sm:grid-cols-4"
|
|
307
|
+
:aria-label="`${title}, ${countLabel}. Alt plus arrow keys move the focused file.`"
|
|
308
|
+
>
|
|
309
|
+
<li
|
|
310
|
+
v-for="(item, i) in items"
|
|
311
|
+
:key="item.key"
|
|
312
|
+
:ref="(el) => tileEls[item.key] = el"
|
|
313
|
+
class="relative min-w-0 rounded-lg transition-[opacity,outline-color] motion-reduce:transition-none"
|
|
314
|
+
:class="[
|
|
315
|
+
highlightClass(item),
|
|
316
|
+
(featured || replaces) && i === 0 ? 'col-span-2 row-span-2' : '',
|
|
317
|
+
dragFrom === i ? 'opacity-40' : '',
|
|
318
|
+
dragOver === i && dragFrom !== i ? 'outline-2 outline-offset-2 outline-primary' : ''
|
|
319
|
+
]"
|
|
320
|
+
:draggable="!!item.id && !disabled"
|
|
321
|
+
@dragstart="onDragStart($event, i)"
|
|
322
|
+
@dragover="onDragOver($event, i)"
|
|
323
|
+
@dragleave="dragOver === i && (dragOver = null)"
|
|
324
|
+
@drop="onDrop($event, i)"
|
|
325
|
+
@dragend="onDragEnd"
|
|
326
|
+
@keydown="onTileKey($event, i)"
|
|
327
|
+
>
|
|
328
|
+
<MLMediaTile
|
|
329
|
+
:asset="item.asset"
|
|
330
|
+
:src="tileSrc(item)"
|
|
331
|
+
:kind="tileKind(item)"
|
|
332
|
+
:name="tileName(item)"
|
|
333
|
+
:missing-alt="missingAlt(item)"
|
|
334
|
+
:selectable="!!item.id && !disabled"
|
|
335
|
+
:selected="!!item.id && selected.has(item.id)"
|
|
336
|
+
:selecting="selected.size > 0"
|
|
337
|
+
:phase="tilePhase(item)"
|
|
338
|
+
:progress="item.upload?.progress ?? null"
|
|
339
|
+
:phase-detail="item.upload && !item.id ? item.upload.file.name : void 0"
|
|
340
|
+
:on-open="openFor(item)"
|
|
341
|
+
@update:selected="(v) => item.id && setSelected(item.id, v)"
|
|
342
|
+
@cancel="onCancel(item)"
|
|
343
|
+
@retry="onRetry(item)"
|
|
344
|
+
>
|
|
345
|
+
<div
|
|
346
|
+
v-if="item.id && badges?.(item.id).length"
|
|
347
|
+
class="pointer-events-none absolute left-8 top-1.5 z-10 flex max-w-[calc(100%-4.5rem)] flex-wrap gap-1"
|
|
348
|
+
>
|
|
349
|
+
<UBadge
|
|
350
|
+
v-for="b in badges(item.id)"
|
|
351
|
+
:key="b"
|
|
352
|
+
:label="b"
|
|
353
|
+
color="neutral"
|
|
354
|
+
variant="solid"
|
|
355
|
+
size="sm"
|
|
356
|
+
class="truncate"
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
<UDropdownMenu
|
|
360
|
+
v-if="item.id && !disabled && menuFor(item).length"
|
|
361
|
+
:items="menuFor(item)"
|
|
362
|
+
:content="{ align: 'end' }"
|
|
363
|
+
>
|
|
364
|
+
<UButton
|
|
365
|
+
icon="i-lucide-ellipsis"
|
|
366
|
+
color="neutral"
|
|
367
|
+
variant="solid"
|
|
368
|
+
size="xs"
|
|
369
|
+
square
|
|
370
|
+
class="absolute bottom-1.5 right-9 z-20 rounded-md opacity-0 shadow-sm transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 data-[state=open]:opacity-100 motion-reduce:transition-none [@media(hover:none)]:opacity-100"
|
|
371
|
+
:aria-label="`More actions for ${tileName(item)}`"
|
|
372
|
+
@click.stop
|
|
373
|
+
/>
|
|
374
|
+
</UDropdownMenu>
|
|
375
|
+
<UBadge
|
|
376
|
+
v-if="item.lookup === 'not_found' || item.lookup === 'forbidden'"
|
|
377
|
+
color="warning"
|
|
378
|
+
variant="solid"
|
|
379
|
+
size="sm"
|
|
380
|
+
label="Unavailable"
|
|
381
|
+
class="absolute bottom-1.5 left-1.5"
|
|
382
|
+
/>
|
|
383
|
+
</MLMediaTile>
|
|
384
|
+
</li>
|
|
385
|
+
|
|
386
|
+
<li v-if="showAddTile">
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
class="flex aspect-square w-full flex-col items-center justify-center gap-1 rounded-lg border-[1.5px] border-dashed border-accented p-2 text-center text-xs text-muted transition-colors hover:border-primary hover:bg-primary/10 hover:text-primary motion-reduce:transition-none"
|
|
390
|
+
@click="canPick ? pickExisting() : open()"
|
|
391
|
+
>
|
|
392
|
+
<UIcon
|
|
393
|
+
name="i-lucide-plus"
|
|
394
|
+
class="size-5"
|
|
395
|
+
/>
|
|
396
|
+
{{ replaces ? "Add file" : "Add media" }}
|
|
397
|
+
<span class="text-[11px] text-dimmed">{{ replaces ? "or drop one" : "or drop files" }}</span>
|
|
398
|
+
</button>
|
|
399
|
+
</li>
|
|
400
|
+
</ul>
|
|
401
|
+
</template>
|
|
402
|
+
</MLMediaDropzone>
|
|
403
|
+
|
|
404
|
+
<p
|
|
405
|
+
v-if="message"
|
|
406
|
+
class="text-xs text-error"
|
|
407
|
+
role="alert"
|
|
408
|
+
>
|
|
409
|
+
{{ message }}
|
|
410
|
+
</p>
|
|
411
|
+
<p
|
|
412
|
+
v-else-if="items.length"
|
|
413
|
+
class="text-xs text-dimmed"
|
|
414
|
+
>
|
|
415
|
+
{{ helpText }}
|
|
416
|
+
</p>
|
|
417
|
+
<span
|
|
418
|
+
class="sr-only"
|
|
419
|
+
aria-live="polite"
|
|
420
|
+
>{{ announcement }}</span>
|
|
421
|
+
</section>
|
|
422
|
+
</template>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { DropdownMenuItem } from '@nuxt/ui';
|
|
2
|
+
import type { MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
/** Asset kinds the list takes. Empty means any file. */
|
|
5
|
+
accept?: readonly string[] | string;
|
|
6
|
+
/** Most files on the record. */
|
|
7
|
+
max?: number | null;
|
|
8
|
+
/** Bytes, per file. */
|
|
9
|
+
maxSize?: number | null;
|
|
10
|
+
title?: string;
|
|
11
|
+
/** Under the grid. Defaults to how ordering works and what the list takes. */
|
|
12
|
+
hint?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Show the first file larger, as the featured one. */
|
|
15
|
+
featured?: boolean;
|
|
16
|
+
/** Used in "Remove from …" labels, e.g. "product". */
|
|
17
|
+
recordName?: string;
|
|
18
|
+
visibility?: MediaUploadOptions['visibility'];
|
|
19
|
+
processingConfig?: MediaUploadOptions['processingConfig'];
|
|
20
|
+
collectionName?: string | null;
|
|
21
|
+
/** Show "Select existing", which opens the library picker. */
|
|
22
|
+
picker?: boolean;
|
|
23
|
+
onPick?: () => void;
|
|
24
|
+
/** Show the eye on each tile, which opens the media editor. */
|
|
25
|
+
editor?: boolean;
|
|
26
|
+
onOpen?: (assetId: string) => void;
|
|
27
|
+
/** Upload profile name for new files in this list. */
|
|
28
|
+
profile?: string | null;
|
|
29
|
+
/** Overrides the "Remove from …" label. */
|
|
30
|
+
removeLabel?: string;
|
|
31
|
+
/** Labels shown on a tile, e.g. its roles. */
|
|
32
|
+
badges?: (assetId: string) => string[];
|
|
33
|
+
/** Extra menu on each tile (⋯). */
|
|
34
|
+
tileMenu?: (assetId: string, kind: string | null) => DropdownMenuItem[][];
|
|
35
|
+
/** When set, these files are highlighted and the rest dimmed. */
|
|
36
|
+
highlight?: ReadonlySet<string> | null;
|
|
37
|
+
/** See useMLMediaGallery's onAdd. */
|
|
38
|
+
onAdd?: (assetId: string, tag: string | undefined) => void;
|
|
39
|
+
};
|
|
40
|
+
/** Upload files with another profile than the list's own, e.g. from a role chip. */
|
|
41
|
+
declare function uploadFiles(files: File[], opts?: {
|
|
42
|
+
profile?: string | null;
|
|
43
|
+
tag?: string;
|
|
44
|
+
accept?: readonly string[] | string;
|
|
45
|
+
maxSize?: number | null;
|
|
46
|
+
maxFiles?: number;
|
|
47
|
+
}): void;
|
|
48
|
+
type __VLS_ModelProps = {
|
|
49
|
+
modelValue?: string[];
|
|
50
|
+
/** Alt text per asset ID, stored on the host record. */
|
|
51
|
+
'alt'?: Record<string, string> | undefined;
|
|
52
|
+
};
|
|
53
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
54
|
+
declare var __VLS_29: {};
|
|
55
|
+
type __VLS_Slots = {} & {
|
|
56
|
+
toolbar?: (props: typeof __VLS_29) => any;
|
|
57
|
+
};
|
|
58
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
59
|
+
uploadFiles: typeof uploadFiles;
|
|
60
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
61
|
+
uploaded: (assetId: string) => any;
|
|
62
|
+
"update:modelValue": (value: string[]) => any;
|
|
63
|
+
"update:alt": (value: Record<string, string> | undefined) => any;
|
|
64
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
65
|
+
onUploaded?: ((assetId: string) => any) | undefined;
|
|
66
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
67
|
+
"onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
|
|
68
|
+
}>, {
|
|
69
|
+
visibility: "private" | "restricted" | "public";
|
|
70
|
+
title: string;
|
|
71
|
+
profile: string | null;
|
|
72
|
+
accept: readonly string[] | string;
|
|
73
|
+
maxSize: number | null;
|
|
74
|
+
processingConfig: {
|
|
75
|
+
stepOptions?: Record<string, Record<string, unknown>> | null;
|
|
76
|
+
useFilenameAsAssetId?: boolean;
|
|
77
|
+
enableTranscription?: boolean;
|
|
78
|
+
};
|
|
79
|
+
collectionName: string | null;
|
|
80
|
+
disabled: boolean;
|
|
81
|
+
highlight: ReadonlySet<string> | null;
|
|
82
|
+
removeLabel: string;
|
|
83
|
+
max: number | null;
|
|
84
|
+
onAdd: (assetId: string, tag: string | undefined) => void;
|
|
85
|
+
hint: string;
|
|
86
|
+
picker: boolean;
|
|
87
|
+
onPick: () => void;
|
|
88
|
+
editor: boolean;
|
|
89
|
+
onOpen: (assetId: string) => void;
|
|
90
|
+
featured: boolean;
|
|
91
|
+
recordName: string;
|
|
92
|
+
badges: (assetId: string) => string[];
|
|
93
|
+
tileMenu: (assetId: string, kind: string | null) => DropdownMenuItem[][];
|
|
94
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
95
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
96
|
+
declare const _default: typeof __VLS_export;
|
|
97
|
+
export default _default;
|
|
98
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
99
|
+
new (): {
|
|
100
|
+
$slots: S;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BadgeProps } from '@nuxt/ui';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
kind: string | null | undefined;
|
|
4
|
+
/** Show the kind's icon before the label. */
|
|
5
|
+
icon?: boolean;
|
|
6
|
+
/** Override the label, e.g. a file extension such as "PDF". */
|
|
7
|
+
label?: string;
|
|
8
|
+
size?: BadgeProps['size'];
|
|
9
|
+
variant?: BadgeProps['variant'];
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
12
|
+
size: "md" | "xs" | "sm" | "lg" | "xl";
|
|
13
|
+
variant: "solid" | "outline" | "soft" | "subtle";
|
|
14
|
+
icon: boolean;
|
|
15
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { assetKindMeta } from "../../utils/assetDisplay";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
kind: { type: null, required: true },
|
|
6
|
+
icon: { type: Boolean, required: false, default: false },
|
|
7
|
+
label: { type: String, required: false },
|
|
8
|
+
size: { type: null, required: false, default: "sm" },
|
|
9
|
+
variant: { type: null, required: false, default: "subtle" }
|
|
10
|
+
});
|
|
11
|
+
const meta = computed(() => assetKindMeta(props.kind));
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<UBadge
|
|
16
|
+
:color="meta.color"
|
|
17
|
+
:variant="variant"
|
|
18
|
+
:size="size"
|
|
19
|
+
:icon="icon ? meta.icon : void 0"
|
|
20
|
+
:label="label ?? meta.label"
|
|
21
|
+
/>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BadgeProps } from '@nuxt/ui';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
kind: string | null | undefined;
|
|
4
|
+
/** Show the kind's icon before the label. */
|
|
5
|
+
icon?: boolean;
|
|
6
|
+
/** Override the label, e.g. a file extension such as "PDF". */
|
|
7
|
+
label?: string;
|
|
8
|
+
size?: BadgeProps['size'];
|
|
9
|
+
variant?: BadgeProps['variant'];
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
12
|
+
size: "md" | "xs" | "sm" | "lg" | "xl";
|
|
13
|
+
variant: "solid" | "outline" | "soft" | "subtle";
|
|
14
|
+
icon: boolean;
|
|
15
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AssetDetail, DeliveryAsset } from '../../types/asset.js';
|
|
2
|
+
import type { ProgressVeilPhase } from './ProgressVeil.vue.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
asset?: AssetDetail | DeliveryAsset | null;
|
|
5
|
+
src?: string | null;
|
|
6
|
+
kind?: string | null;
|
|
7
|
+
name?: string;
|
|
8
|
+
/** Seconds. Read from the asset's renditions when omitted. */
|
|
9
|
+
duration?: number | null;
|
|
10
|
+
/** Shows the "No alt" warning. Only meaningful for images; the host knows where alt text lives. */
|
|
11
|
+
missingAlt?: boolean;
|
|
12
|
+
/** Shows the selection checkbox on hover and focus; always visible once anything is selected. */
|
|
13
|
+
selectable?: boolean;
|
|
14
|
+
selected?: boolean;
|
|
15
|
+
/** Put the checkbox on screen without hovering, e.g. while the gallery is in selection mode. */
|
|
16
|
+
selecting?: boolean;
|
|
17
|
+
/** Overlay for uploads and processing. Omit when the file is ready. */
|
|
18
|
+
phase?: ProgressVeilPhase | null;
|
|
19
|
+
progress?: number | null;
|
|
20
|
+
phaseDetail?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Fixed shape for grid cells. */
|
|
23
|
+
aspect?: 'square' | 'landscape' | 'portrait' | 'video';
|
|
24
|
+
/** Passing a listener shows the eye button, which opens the editor. Enter does the same. */
|
|
25
|
+
onOpen?: () => void;
|
|
26
|
+
};
|
|
27
|
+
type __VLS_Slots = {
|
|
28
|
+
/** Extra overlays, e.g. a drag handle or a "Featured" label. */
|
|
29
|
+
default?: () => unknown;
|
|
30
|
+
};
|
|
31
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
32
|
+
cancel: () => any;
|
|
33
|
+
retry: () => any;
|
|
34
|
+
"update:selected": (value: boolean) => any;
|
|
35
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
36
|
+
onCancel?: (() => any) | undefined;
|
|
37
|
+
onRetry?: (() => any) | undefined;
|
|
38
|
+
"onUpdate:selected"?: ((value: boolean) => any) | undefined;
|
|
39
|
+
}>, {
|
|
40
|
+
asset: AssetDetail | DeliveryAsset | null;
|
|
41
|
+
src: string | null;
|
|
42
|
+
progress: number | null;
|
|
43
|
+
name: string;
|
|
44
|
+
kind: string | null;
|
|
45
|
+
phase: ProgressVeilPhase | null;
|
|
46
|
+
disabled: boolean;
|
|
47
|
+
selected: boolean;
|
|
48
|
+
missingAlt: boolean;
|
|
49
|
+
onOpen: () => void;
|
|
50
|
+
duration: number | null;
|
|
51
|
+
selectable: boolean;
|
|
52
|
+
selecting: boolean;
|
|
53
|
+
phaseDetail: string;
|
|
54
|
+
aspect: "square" | "landscape" | "portrait" | "video";
|
|
55
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
56
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
57
|
+
declare const _default: typeof __VLS_export;
|
|
58
|
+
export default _default;
|
|
59
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
60
|
+
new (): {
|
|
61
|
+
$slots: S;
|
|
62
|
+
};
|
|
63
|
+
};
|