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.
Files changed (87) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/MLMedia/Dropzone.d.vue.ts +60 -0
  3. package/dist/runtime/components/MLMedia/Dropzone.vue +137 -0
  4. package/dist/runtime/components/MLMedia/Dropzone.vue.d.ts +60 -0
  5. package/dist/runtime/components/MLMedia/Editor.d.vue.ts +23 -0
  6. package/dist/runtime/components/MLMedia/Editor.vue +34 -0
  7. package/dist/runtime/components/MLMedia/Editor.vue.d.ts +23 -0
  8. package/dist/runtime/components/MLMedia/EditorPanel.d.vue.ts +38 -0
  9. package/dist/runtime/components/MLMedia/EditorPanel.vue +220 -0
  10. package/dist/runtime/components/MLMedia/EditorPanel.vue.d.ts +38 -0
  11. package/dist/runtime/components/MLMedia/Field.d.vue.ts +65 -0
  12. package/dist/runtime/components/MLMedia/Field.vue +282 -0
  13. package/dist/runtime/components/MLMedia/Field.vue.d.ts +65 -0
  14. package/dist/runtime/components/MLMedia/FileFacts.d.vue.ts +19 -0
  15. package/dist/runtime/components/MLMedia/FileFacts.vue +38 -0
  16. package/dist/runtime/components/MLMedia/FileFacts.vue.d.ts +19 -0
  17. package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +23 -0
  18. package/dist/runtime/components/MLMedia/FilePicker.vue +33 -0
  19. package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +23 -0
  20. package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +33 -0
  21. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +300 -0
  22. package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +33 -0
  23. package/dist/runtime/components/MLMedia/FilterPill.d.vue.ts +30 -0
  24. package/dist/runtime/components/MLMedia/FilterPill.vue +84 -0
  25. package/dist/runtime/components/MLMedia/FilterPill.vue.d.ts +30 -0
  26. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +78 -0
  27. package/dist/runtime/components/MLMedia/Gallery.vue +256 -0
  28. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +78 -0
  29. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +102 -0
  30. package/dist/runtime/components/MLMedia/GallerySection.vue +422 -0
  31. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +102 -0
  32. package/dist/runtime/components/MLMedia/KindBadge.d.vue.ts +17 -0
  33. package/dist/runtime/components/MLMedia/KindBadge.vue +22 -0
  34. package/dist/runtime/components/MLMedia/KindBadge.vue.d.ts +17 -0
  35. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +63 -0
  36. package/dist/runtime/components/MLMedia/MediaTile.vue +117 -0
  37. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +63 -0
  38. package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +42 -0
  39. package/dist/runtime/components/MLMedia/PickerCard.vue +79 -0
  40. package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +42 -0
  41. package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +31 -0
  42. package/dist/runtime/components/MLMedia/PickerRow.vue +65 -0
  43. package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +31 -0
  44. package/dist/runtime/components/MLMedia/PreviewEye.d.vue.ts +16 -0
  45. package/dist/runtime/components/MLMedia/PreviewEye.vue +21 -0
  46. package/dist/runtime/components/MLMedia/PreviewEye.vue.d.ts +16 -0
  47. package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +25 -0
  48. package/dist/runtime/components/MLMedia/ProgressVeil.vue +98 -0
  49. package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +25 -0
  50. package/dist/runtime/components/MLMedia/StatusBadge.d.vue.ts +17 -0
  51. package/dist/runtime/components/MLMedia/StatusBadge.vue +31 -0
  52. package/dist/runtime/components/MLMedia/StatusBadge.vue.d.ts +17 -0
  53. package/dist/runtime/components/MLMedia/Thumbnail.d.vue.ts +36 -0
  54. package/dist/runtime/components/MLMedia/Thumbnail.vue +84 -0
  55. package/dist/runtime/components/MLMedia/Thumbnail.vue.d.ts +36 -0
  56. package/dist/runtime/components/MLMedia/UsageList.d.vue.ts +26 -0
  57. package/dist/runtime/components/MLMedia/UsageList.vue +66 -0
  58. package/dist/runtime/components/MLMedia/UsageList.vue.d.ts +26 -0
  59. package/dist/runtime/components/MLProcessing/StepLayout.d.vue.ts +1 -1
  60. package/dist/runtime/components/MLProcessing/StepLayout.vue.d.ts +1 -1
  61. package/dist/runtime/composables/useMLFilePicker.d.ts +20 -0
  62. package/dist/runtime/composables/useMLFilePicker.js +17 -0
  63. package/dist/runtime/composables/useMLMediaAssets.d.ts +19 -0
  64. package/dist/runtime/composables/useMLMediaAssets.js +56 -0
  65. package/dist/runtime/composables/useMLMediaBrowse.d.ts +147 -0
  66. package/dist/runtime/composables/useMLMediaBrowse.js +72 -0
  67. package/dist/runtime/composables/useMLMediaEditor.d.ts +22 -0
  68. package/dist/runtime/composables/useMLMediaEditor.js +20 -0
  69. package/dist/runtime/composables/useMLMediaField.d.ts +29 -0
  70. package/dist/runtime/composables/useMLMediaField.js +77 -0
  71. package/dist/runtime/composables/useMLMediaGallery.d.ts +55 -0
  72. package/dist/runtime/composables/useMLMediaGallery.js +134 -0
  73. package/dist/runtime/composables/useMLMediaUpload.d.ts +134 -0
  74. package/dist/runtime/composables/useMLMediaUpload.js +153 -0
  75. package/dist/runtime/index.d.ts +11 -0
  76. package/dist/runtime/index.js +11 -0
  77. package/dist/runtime/utils/assetDisplay.d.ts +8 -0
  78. package/dist/runtime/utils/assetDisplay.js +29 -0
  79. package/dist/runtime/utils/assetFormat.d.ts +30 -0
  80. package/dist/runtime/utils/assetFormat.js +69 -0
  81. package/dist/runtime/utils/assetThumbnail.d.ts +11 -0
  82. package/dist/runtime/utils/assetThumbnail.js +14 -0
  83. package/dist/runtime/utils/fileAccept.d.ts +26 -0
  84. package/dist/runtime/utils/fileAccept.js +104 -0
  85. package/dist/runtime/utils/galleryDrag.d.ts +2 -0
  86. package/dist/runtime/utils/galleryDrag.js +1 -0
  87. 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
+ };