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,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;