nuxt-media-library 0.0.63 → 0.0.65

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 (37) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/MLMedia/Browser.d.vue.ts +98 -0
  3. package/dist/runtime/components/MLMedia/Browser.vue +521 -0
  4. package/dist/runtime/components/MLMedia/Browser.vue.d.ts +98 -0
  5. package/dist/runtime/components/MLMedia/Field.d.vue.ts +7 -1
  6. package/dist/runtime/components/MLMedia/Field.vue +2 -0
  7. package/dist/runtime/components/MLMedia/Field.vue.d.ts +7 -1
  8. package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +2 -0
  9. package/dist/runtime/components/MLMedia/FilePicker.vue +2 -0
  10. package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +2 -0
  11. package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +7 -1
  12. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +34 -284
  13. package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +7 -1
  14. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +18 -1
  15. package/dist/runtime/components/MLMedia/Gallery.vue +56 -7
  16. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +18 -1
  17. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +6 -1
  18. package/dist/runtime/components/MLMedia/GallerySection.vue +8 -3
  19. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +6 -1
  20. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +1 -1
  21. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +1 -1
  22. package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +7 -0
  23. package/dist/runtime/components/MLMedia/PickerCard.vue +17 -1
  24. package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +7 -0
  25. package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +16 -1
  26. package/dist/runtime/components/MLMedia/PickerRow.vue +47 -30
  27. package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +16 -1
  28. package/dist/runtime/components/MLProcessing/StepLayout.d.vue.ts +1 -1
  29. package/dist/runtime/components/MLProcessing/StepLayout.vue.d.ts +1 -1
  30. package/dist/runtime/composables/useMLFilePicker.d.ts +6 -0
  31. package/dist/runtime/composables/useMLMediaBrowse.d.ts +25 -5
  32. package/dist/runtime/composables/useMLMediaBrowse.js +26 -0
  33. package/dist/runtime/utils/browserActions.d.ts +47 -0
  34. package/dist/runtime/utils/browserActions.js +36 -0
  35. package/dist/runtime/utils/galleryRoles.d.ts +7 -0
  36. package/dist/runtime/utils/galleryRoles.js +11 -0
  37. package/package.json +2 -1
@@ -0,0 +1,98 @@
1
+ import type { AssetDetail } from '../../types/asset.js';
2
+ import { type MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
3
+ import { type MediaEditorResult } from '../../composables/useMLMediaEditor.js';
4
+ import { type MediaBrowserAction, type MediaBrowserRole } from '../../utils/browserActions.js';
5
+ export type { MediaBrowserAction, MediaBrowserRole };
6
+ type __VLS_Props = {
7
+ /**
8
+ * List only these asset IDs (resolved through `backend`'s `resolve`
9
+ * route, searched and filtered here) instead of the library. A pool the
10
+ * host owns, e.g. a course's files. null lists the library.
11
+ */
12
+ pickFrom?: readonly string[] | null;
13
+ /** Backend whose `resolve` route reads the files. Defaults to the upload backend. */
14
+ backend?: string | null;
15
+ /** Kinds that can be selected. Others are listed but dimmed. Empty means any. */
16
+ accept?: readonly string[] | string;
17
+ /** 'none' shows no checkboxes (a click previews); 'single' replaces; 'multi' adds up to `max`. */
18
+ selection?: 'none' | 'single' | 'multi';
19
+ /** Most files that can be selected in 'multi'. */
20
+ max?: number;
21
+ /** IDs listed first (when not searching), e.g. the files a record already has. */
22
+ pinned?: readonly string[];
23
+ /** Upload session settings; shows the dropzone. false hides it. */
24
+ upload?: MediaUploadOptions | false;
25
+ /** Bytes, for uploads. */
26
+ maxSize?: number | null;
27
+ /**
28
+ * What can be done to a file, from its menu and (for several) the bar over
29
+ * the selection: 'preview', 'edit', 'remove', 'delete',
30
+ * `{ key: 'assign', roles }`, or the host's own `{ key, label, … }`.
31
+ */
32
+ actions?: readonly MediaBrowserAction[];
33
+ /** Grid rows for a fixed-height body; the list scrolls inside it. */
34
+ bodyClass?: string;
35
+ };
36
+ type __VLS_Slots = {
37
+ /** Next to the search box, e.g. "Add from library". */
38
+ 'toolbar'?: () => unknown;
39
+ /** Small labels on a card or row, e.g. "Used in 3 lessons". */
40
+ 'card-badge'?: (p: {
41
+ asset: AssetDetail;
42
+ }) => unknown;
43
+ /** Extra controls on a card or row, next to its menu. */
44
+ 'card-actions'?: (p: {
45
+ asset: AssetDetail;
46
+ }) => unknown;
47
+ /** Replaces the empty state. */
48
+ 'empty'?: (p: {
49
+ filtering: boolean;
50
+ }) => unknown;
51
+ /** Under the list, outside the scroll, e.g. the picker's Done / Cancel. */
52
+ 'footer'?: (p: {
53
+ selected: string[];
54
+ summary: string;
55
+ note: string | null;
56
+ }) => unknown;
57
+ };
58
+ type __VLS_ModelProps = {
59
+ /** Selected IDs, in the order they were chosen. Two-way and live. */
60
+ 'selected'?: string[];
61
+ };
62
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
63
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
64
+ delete: (assetIds: string[]) => any;
65
+ remove: (assetIds: string[]) => any;
66
+ uploaded: (assetId: string, sessionId: string | null) => any;
67
+ action: (key: string, assetIds: string[], role?: string | undefined) => any;
68
+ edit: (assetId: string, result: MediaEditorResult | null) => any;
69
+ assign: (role: string, assetIds: string[]) => any;
70
+ "update:selected": (value: string[]) => any;
71
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
72
+ onDelete?: ((assetIds: string[]) => any) | undefined;
73
+ onRemove?: ((assetIds: string[]) => any) | undefined;
74
+ onUploaded?: ((assetId: string, sessionId: string | null) => any) | undefined;
75
+ onAction?: ((key: string, assetIds: string[], role?: string | undefined) => any) | undefined;
76
+ onEdit?: ((assetId: string, result: MediaEditorResult | null) => any) | undefined;
77
+ onAssign?: ((role: string, assetIds: string[]) => any) | undefined;
78
+ "onUpdate:selected"?: ((value: string[]) => any) | undefined;
79
+ }>, {
80
+ upload: MediaUploadOptions | false;
81
+ actions: readonly MediaBrowserAction[];
82
+ backend: string | null;
83
+ accept: readonly string[] | string;
84
+ max: number;
85
+ maxSize: number | null;
86
+ pickFrom: readonly string[] | null;
87
+ selection: "none" | "single" | "multi";
88
+ pinned: readonly string[];
89
+ bodyClass: string;
90
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
91
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
92
+ declare const _default: typeof __VLS_export;
93
+ export default _default;
94
+ type __VLS_WithSlots<T, S> = T & {
95
+ new (): {
96
+ $slots: S;
97
+ };
98
+ };
@@ -32,6 +32,11 @@ type __VLS_Props = {
32
32
  picker?: boolean;
33
33
  /** Replaces the built-in picker, e.g. with the host's own dialog. */
34
34
  onPick?: () => void;
35
+ /**
36
+ * The picker lists only these asset IDs (resolved through the backend's
37
+ * `resolve` route) instead of the library. For backends without a library listing.
38
+ */
39
+ pickFrom?: readonly string[] | null;
35
40
  /** Show the eye button on the thumbnail, which opens the media editor. */
36
41
  editor?: boolean;
37
42
  /** Label for the editor's remove button. Empty hides it. */
@@ -78,12 +83,13 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
78
83
  serviceBody: Record<string, unknown> | null;
79
84
  accept: readonly string[] | string;
80
85
  maxSize: number | null;
86
+ pickFrom: readonly string[] | null;
81
87
  required: boolean;
82
88
  removeLabel: string;
89
+ onPick: () => void;
83
90
  hint: string;
84
91
  missingAlt: boolean;
85
92
  picker: boolean;
86
- onPick: () => void;
87
93
  editor: boolean;
88
94
  onOpen: (assetId: string) => void;
89
95
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -22,6 +22,7 @@ const props = defineProps({
22
22
  serviceBody: { type: [Object, null], required: false, default: null },
23
23
  picker: { type: Boolean, required: false, default: true },
24
24
  onPick: { type: Function, required: false, default: void 0 },
25
+ pickFrom: { type: [Array, null], required: false, default: null },
25
26
  editor: { type: Boolean, required: false, default: true },
26
27
  removeLabel: { type: String, required: false, default: "Remove from this field" },
27
28
  onOpen: { type: Function, required: false, default: void 0 }
@@ -101,6 +102,7 @@ async function pickExisting() {
101
102
  selected: [model.value],
102
103
  title: props.label ? `Select ${props.label.toLowerCase()}` : "Select file",
103
104
  maxSize: props.maxSize,
105
+ pickFrom: props.pickFrom,
104
106
  upload: uploadOptions.value,
105
107
  onUploaded: (id, sessionId) => sessions.set(id, sessionId)
106
108
  });
@@ -32,6 +32,11 @@ type __VLS_Props = {
32
32
  picker?: boolean;
33
33
  /** Replaces the built-in picker, e.g. with the host's own dialog. */
34
34
  onPick?: () => void;
35
+ /**
36
+ * The picker lists only these asset IDs (resolved through the backend's
37
+ * `resolve` route) instead of the library. For backends without a library listing.
38
+ */
39
+ pickFrom?: readonly string[] | null;
35
40
  /** Show the eye button on the thumbnail, which opens the media editor. */
36
41
  editor?: boolean;
37
42
  /** Label for the editor's remove button. Empty hides it. */
@@ -78,12 +83,13 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
78
83
  serviceBody: Record<string, unknown> | null;
79
84
  accept: readonly string[] | string;
80
85
  maxSize: number | null;
86
+ pickFrom: readonly string[] | null;
81
87
  required: boolean;
82
88
  removeLabel: string;
89
+ onPick: () => void;
83
90
  hint: string;
84
91
  missingAlt: boolean;
85
92
  picker: boolean;
86
- onPick: () => void;
87
93
  editor: boolean;
88
94
  onOpen: (assetId: string) => void;
89
95
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -5,6 +5,7 @@ type __VLS_Props = {
5
5
  selected?: readonly string[];
6
6
  title?: string;
7
7
  maxSize?: number | null;
8
+ pickFrom?: readonly string[] | null;
8
9
  upload?: MediaUploadOptions;
9
10
  onUploaded?: (assetId: string, sessionId: string | null) => void;
10
11
  };
@@ -19,6 +20,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
19
20
  max: number;
20
21
  selected: readonly string[];
21
22
  maxSize: number | null;
23
+ pickFrom: readonly string[] | null;
22
24
  onUploaded: (assetId: string, sessionId: string | null) => void;
23
25
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
26
  declare const _default: typeof __VLS_export;
@@ -5,6 +5,7 @@ defineProps({
5
5
  selected: { type: Array, required: false, default: () => [] },
6
6
  title: { type: String, required: false, default: "Select file" },
7
7
  maxSize: { type: [Number, null], required: false, default: null },
8
+ pickFrom: { type: [Array, null], required: false, default: null },
8
9
  upload: { type: Object, required: false, default: () => ({}) },
9
10
  onUploaded: { type: Function, required: false, default: void 0 }
10
11
  });
@@ -25,6 +26,7 @@ const emit = defineEmits(["close"]);
25
26
  :max="max"
26
27
  :selected="selected"
27
28
  :max-size="maxSize"
29
+ :pick-from="pickFrom"
28
30
  :upload="upload"
29
31
  :on-uploaded="onUploaded"
30
32
  @done="(ids) => emit('close', ids)"
@@ -5,6 +5,7 @@ type __VLS_Props = {
5
5
  selected?: readonly string[];
6
6
  title?: string;
7
7
  maxSize?: number | null;
8
+ pickFrom?: readonly string[] | null;
8
9
  upload?: MediaUploadOptions;
9
10
  onUploaded?: (assetId: string, sessionId: string | null) => void;
10
11
  };
@@ -19,6 +20,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
19
20
  max: number;
20
21
  selected: readonly string[];
21
22
  maxSize: number | null;
23
+ pickFrom: readonly string[] | null;
22
24
  onUploaded: (assetId: string, sessionId: string | null) => void;
23
25
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
26
  declare const _default: typeof __VLS_export;
@@ -1,4 +1,4 @@
1
- import { type MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
1
+ import type { MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
2
2
  type __VLS_Props = {
3
3
  /** Kinds the caller takes. Others are listed but dimmed. Empty means any. */
4
4
  accept?: readonly string[] | string;
@@ -9,6 +9,11 @@ type __VLS_Props = {
9
9
  title?: string;
10
10
  /** Bytes, for uploads from the picker. */
11
11
  maxSize?: number | null;
12
+ /**
13
+ * List only these asset IDs (resolved through `upload.backend`'s `resolve`
14
+ * route) instead of the library. Search and the kind filter run here.
15
+ */
16
+ pickFrom?: readonly string[] | null;
12
17
  /** Upload session settings for files dropped into the picker. */
13
18
  upload?: MediaUploadOptions;
14
19
  /** A file uploaded from the picker exists in the library (with its upload session). */
@@ -29,6 +34,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
29
34
  max: number;
30
35
  selected: readonly string[];
31
36
  maxSize: number | null;
37
+ pickFrom: readonly string[] | null;
32
38
  onUploaded: (assetId: string, sessionId: string | null) => void;
33
39
  bodyClass: string;
34
40
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,306 +1,56 @@
1
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"];
2
+ import { computed, ref } from "vue";
3
+ import MLMediaBrowser from "./Browser.vue";
11
4
  const props = defineProps({
12
5
  accept: { type: [Array, String], required: false, default: "" },
13
6
  max: { type: Number, required: false, default: Number.POSITIVE_INFINITY },
14
7
  selected: { type: Array, required: false, default: () => [] },
15
8
  title: { type: String, required: false, default: void 0 },
16
9
  maxSize: { type: [Number, null], required: false, default: null },
10
+ pickFrom: { type: [Array, null], required: false, default: null },
17
11
  upload: { type: Object, required: false, default: () => ({}) },
18
12
  onUploaded: { type: Function, required: false, default: void 0 },
19
13
  bodyClass: { type: String, required: false, default: "max-h-[min(60vh,560px)]" }
20
14
  });
21
15
  const emit = defineEmits(["done", "cancel"]);
22
- const acceptKinds = computed(() => {
23
- const raw = typeof props.accept === "string" ? props.accept.split(",") : [...props.accept];
24
- return raw.map((k) => k.trim().toLowerCase()).filter(Boolean);
25
- });
26
- const isAccepted = (kind) => !acceptKinds.value.length || acceptKinds.value.includes((kind ?? "").toLowerCase());
27
- const notAcceptedReason = computed(() => `Only ${describeKinds(acceptKinds.value)} can be chosen here`);
28
- const search = ref("");
29
- const view = ref("grid");
30
- const kindFilter = ref(acceptKinds.value.length === 1 ? [acceptKinds.value[0]] : []);
31
- const kindOptions = computed(
32
- () => (acceptKinds.value.length ? acceptKinds.value : ALL_KINDS).map((k) => ({ label: assetKindMeta(k).label, value: k }))
33
- );
34
- const filtering = computed(() => !!search.value.trim() || kindFilter.value.length > 0);
35
- const browse = useMLMediaBrowse(() => ({ kind: kindFilter.value[0] ?? null, query: search.value }));
36
16
  const initial = [...props.selected];
37
17
  const chosen = ref([...initial]);
38
- const limitNote = ref(null);
39
- const single = computed(() => props.max === 1);
40
18
  const changed = computed(() => chosen.value.length !== initial.length || chosen.value.some((id, i) => id !== initial[i]));
41
- function isChosen(id) {
42
- return !!id && chosen.value.includes(id);
43
- }
44
- function choose(id) {
45
- limitNote.value = null;
46
- if (single.value) {
47
- chosen.value = [id];
48
- return true;
49
- }
50
- if (chosen.value.length >= props.max) {
51
- limitNote.value = `You can choose up to ${props.max}. Unselect one first.`;
52
- return false;
53
- }
54
- chosen.value = [...chosen.value, id];
55
- return true;
56
- }
57
- function toggle(id) {
58
- if (isChosen(id)) {
59
- limitNote.value = null;
60
- chosen.value = chosen.value.filter((x) => x !== id);
61
- } else {
62
- choose(id);
63
- }
64
- }
65
- const pinned = useMLMediaAssets(() => initial, { backend: () => props.upload?.backend });
66
- const pinnedAssets = computed(() => initial.map((id) => pinned.get(id)).filter((a) => !!a));
67
- const uploader = useMLMediaUpload(props.upload);
68
- const rejectNote = ref(null);
69
- function onFiles(files) {
70
- rejectNote.value = null;
71
- for (const file of files) {
72
- const u = uploader.start(file);
73
- void u.created.then((id) => {
74
- if (!id) return;
75
- u.attached = true;
76
- props.onUploaded?.(id, u.sessionId);
77
- if (!isChosen(id)) choose(id);
78
- });
79
- }
80
- }
81
- function onRejected(rejected) {
82
- const first = rejected[0];
83
- if (!first) return;
84
- const more = rejected.length > 1 ? ` (and ${rejected.length - 1} more)` : "";
85
- 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"}.`;
86
- }
87
- const uploadIds = computed(() => new Set(uploader.uploads.value.map((u) => u.assetId).filter(Boolean)));
88
- const visibleUploads = computed(() => uploader.uploads.value.filter((u) => u.phase !== "canceled"));
89
- function uploadPhase(u) {
90
- return u.phase === "uploading" || u.phase === "processing" || u.phase === "failed" ? u.phase : null;
91
- }
92
- const listed = computed(() => {
93
- const pinnedIds = new Set(filtering.value ? [] : pinnedAssets.value.map((a) => a.id));
94
- const rest = browse.items.value.filter((a) => !pinnedIds.has(a.id) && !uploadIds.value.has(a.id));
95
- return filtering.value ? rest : [...pinnedAssets.value, ...rest];
96
- });
97
- const initialLoading = computed(() => browse.loading.value && !browse.items.value.length);
98
- const isEmpty = computed(() => !initialLoading.value && !listed.value.length && !visibleUploads.value.length);
99
- watch(kindFilter, () => {
100
- limitNote.value = null;
101
- });
102
- const mediaEditor = useMLMediaEditor();
103
- function preview(id) {
104
- void mediaEditor.open(id, { readonly: true, siblings: listed.value.map((a) => a.id) });
105
- }
106
- const footerText = computed(() => {
107
- if (limitNote.value) return limitNote.value;
108
- const n = chosen.value.length;
109
- if (single.value) return n ? "1 selected" : "Nothing selected";
110
- return Number.isFinite(props.max) ? `${n} of ${props.max} selected` : `${n} selected`;
111
- });
112
19
  </script>
113
20
 
114
21
  <template>
115
- <div class="flex min-h-0 flex-col">
116
- <div class="flex flex-col gap-3 border-b border-default p-4">
117
- <div class="flex flex-wrap items-center gap-2">
118
- <UInput
119
- v-model="search"
120
- icon="i-lucide-search"
121
- placeholder="Search files"
122
- class="min-w-0 flex-1 basis-56"
123
- :loading="browse.loading.value && !!search"
124
- aria-label="Search files"
22
+ <MLMediaBrowser
23
+ v-model:selected="chosen"
24
+ :pick-from="pickFrom"
25
+ :accept="accept"
26
+ :selection="max === 1 ? 'single' : 'multi'"
27
+ :max="max"
28
+ :pinned="initial"
29
+ :upload="upload"
30
+ :max-size="maxSize"
31
+ :actions="['preview']"
32
+ :body-class="bodyClass"
33
+ @uploaded="(id, sessionId) => props.onUploaded?.(id, sessionId)"
34
+ >
35
+ <template #footer="{ summary, note }">
36
+ <div class="flex flex-wrap items-center gap-2 border-t border-default px-4 py-3">
37
+ <span
38
+ class="me-auto text-sm"
39
+ :class="note ? 'text-warning' : 'text-muted'"
40
+ aria-live="polite"
41
+ >{{ summary }}</span>
42
+ <UButton
43
+ color="neutral"
44
+ variant="outline"
45
+ label="Cancel"
46
+ @click="emit('cancel')"
125
47
  />
126
- <UFieldGroup size="md">
127
- <UButton
128
- icon="i-lucide-layout-grid"
129
- color="neutral"
130
- :variant="view === 'grid' ? 'soft' : 'outline'"
131
- aria-label="Grid view"
132
- :aria-pressed="view === 'grid'"
133
- @click="view = 'grid'"
134
- />
135
- <UButton
136
- icon="i-lucide-list"
137
- color="neutral"
138
- :variant="view === 'list' ? 'soft' : 'outline'"
139
- aria-label="List view"
140
- :aria-pressed="view === 'list'"
141
- @click="view = 'list'"
142
- />
143
- </UFieldGroup>
144
- </div>
145
- <div
146
- v-if="kindOptions.length > 1"
147
- class="flex flex-wrap gap-1.5"
148
- >
149
- <MLMediaFilterPill
150
- v-model="kindFilter"
151
- label="File type"
152
- :options="kindOptions"
153
- single
48
+ <UButton
49
+ label="Done"
50
+ :disabled="!changed"
51
+ @click="emit('done', [...chosen])"
154
52
  />
155
53
  </div>
156
- </div>
157
-
158
- <div
159
- class="flex min-h-0 flex-col gap-3 overflow-y-auto p-4"
160
- :class="bodyClass"
161
- >
162
- <MLMediaDropzone
163
- :accept="acceptForKinds(acceptKinds)"
164
- :multiple="!single"
165
- :max-size="maxSize"
166
- compact
167
- :title="single ? 'Drop a file to add it to the library' : 'Drop files to add them to the library'"
168
- button-label="Upload from computer"
169
- @files="onFiles"
170
- @rejected="onRejected"
171
- />
172
- <p
173
- v-if="rejectNote"
174
- class="text-xs text-error"
175
- >
176
- {{ rejectNote }}
177
- </p>
178
-
179
- <UAlert
180
- v-if="browse.error.value"
181
- color="error"
182
- variant="subtle"
183
- icon="i-lucide-circle-alert"
184
- :title="browse.error.value"
185
- :actions="[{ label: 'Try again', color: 'neutral', variant: 'outline', onClick: () => browse.reload() }]"
186
- />
187
-
188
- <!-- Loading -->
189
- <div
190
- v-if="initialLoading"
191
- class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
192
- >
193
- <div
194
- v-for="i in 12"
195
- :key="i"
196
- class="flex flex-col items-center gap-2 p-2.5"
197
- >
198
- <USkeleton class="aspect-square w-full max-w-28 rounded-md" />
199
- <USkeleton class="h-3 w-3/4" />
200
- </div>
201
- </div>
202
-
203
- <!-- Empty -->
204
- <UEmpty
205
- v-else-if="isEmpty && !browse.error.value"
206
- variant="naked"
207
- :icon="filtering ? 'i-lucide-search-x' : 'i-lucide-folder-open'"
208
- :title="filtering ? 'No files match' : 'No files yet'"
209
- :description="filtering ? 'Try another search or clear the file type.' : 'Drop files above to add the first one.'"
210
- />
211
-
212
- <!-- Grid -->
213
- <div
214
- v-else-if="view === 'grid'"
215
- class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
216
- >
217
- <MLMediaPickerCard
218
- v-for="u in visibleUploads"
219
- :key="u.key"
220
- :src="u.localPreview"
221
- :kind="u.kind"
222
- :name="u.file.name"
223
- :meta="u.phase === 'failed' ? u.error ?? 'Failed' : void 0"
224
- :selected="isChosen(u.assetId)"
225
- :disabled="!u.assetId || !isAccepted(u.kind)"
226
- @toggle="u.assetId && toggle(u.assetId)"
227
- >
228
- <MLMediaProgressVeil
229
- v-if="uploadPhase(u)"
230
- :phase="uploadPhase(u)"
231
- :progress="u.progress"
232
- compact
233
- />
234
- </MLMediaPickerCard>
235
- <MLMediaPickerCard
236
- v-for="a in listed"
237
- :key="a.id"
238
- :asset="a"
239
- :selected="isChosen(a.id)"
240
- :disabled="!isAccepted(a.kind) || a.status === 'failed'"
241
- :disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
242
- :on-preview="() => preview(a.id)"
243
- @toggle="toggle(a.id)"
244
- />
245
- </div>
246
-
247
- <!-- List -->
248
- <div
249
- v-else
250
- class="flex flex-col divide-y divide-default overflow-hidden rounded-lg border border-default"
251
- >
252
- <MLMediaPickerRow
253
- v-for="u in visibleUploads"
254
- :key="u.key"
255
- :src="u.localPreview"
256
- :kind="u.kind"
257
- :name="u.file.name"
258
- :size="u.phase === 'uploading' ? `${Math.round(u.progress ?? 0)}%` : formatBytes(u.file.size)"
259
- :date="u.phase === 'ready' ? 'Just now' : u.phase === 'failed' ? 'Failed' : 'Uploading'"
260
- :selected="isChosen(u.assetId)"
261
- :disabled="!u.assetId || !isAccepted(u.kind)"
262
- @toggle="u.assetId && toggle(u.assetId)"
263
- />
264
- <MLMediaPickerRow
265
- v-for="a in listed"
266
- :key="a.id"
267
- :asset="a"
268
- :selected="isChosen(a.id)"
269
- :disabled="!isAccepted(a.kind) || a.status === 'failed'"
270
- :disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
271
- @toggle="toggle(a.id)"
272
- />
273
- </div>
274
-
275
- <UButton
276
- v-if="browse.hasMore.value && !initialLoading"
277
- class="self-center"
278
- color="neutral"
279
- variant="outline"
280
- size="sm"
281
- label="Load more"
282
- :loading="browse.loading.value"
283
- @click="browse.loadMore()"
284
- />
285
- </div>
286
-
287
- <div class="flex flex-wrap items-center gap-2 border-t border-default px-4 py-3">
288
- <span
289
- class="me-auto text-sm"
290
- :class="limitNote ? 'text-warning' : 'text-muted'"
291
- aria-live="polite"
292
- >{{ footerText }}</span>
293
- <UButton
294
- color="neutral"
295
- variant="outline"
296
- label="Cancel"
297
- @click="emit('cancel')"
298
- />
299
- <UButton
300
- label="Done"
301
- :disabled="!changed"
302
- @click="emit('done', [...chosen])"
303
- />
304
- </div>
305
- </div>
54
+ </template>
55
+ </MLMediaBrowser>
306
56
  </template>
@@ -1,4 +1,4 @@
1
- import { type MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
1
+ import type { MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
2
2
  type __VLS_Props = {
3
3
  /** Kinds the caller takes. Others are listed but dimmed. Empty means any. */
4
4
  accept?: readonly string[] | string;
@@ -9,6 +9,11 @@ type __VLS_Props = {
9
9
  title?: string;
10
10
  /** Bytes, for uploads from the picker. */
11
11
  maxSize?: number | null;
12
+ /**
13
+ * List only these asset IDs (resolved through `upload.backend`'s `resolve`
14
+ * route) instead of the library. Search and the kind filter run here.
15
+ */
16
+ pickFrom?: readonly string[] | null;
12
17
  /** Upload session settings for files dropped into the picker. */
13
18
  upload?: MediaUploadOptions;
14
19
  /** A file uploaded from the picker exists in the library (with its upload session). */
@@ -29,6 +34,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
29
34
  max: number;
30
35
  selected: readonly string[];
31
36
  maxSize: number | null;
37
+ pickFrom: readonly string[] | null;
32
38
  onUploaded: (assetId: string, sessionId: string | null) => void;
33
39
  bodyClass: string;
34
40
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -47,6 +47,12 @@ type __VLS_Props = {
47
47
  serviceBody?: Record<string, unknown> | null;
48
48
  picker?: boolean;
49
49
  onPick?: () => void;
50
+ /**
51
+ * The picker lists only these asset IDs (resolved through the backend's
52
+ * `resolve` route, searched and filtered here) instead of the library.
53
+ * For backends without a library listing.
54
+ */
55
+ pickFrom?: readonly string[] | null;
50
56
  editor?: boolean;
51
57
  onOpen?: (assetId: string) => void;
52
58
  };
@@ -58,6 +64,12 @@ type __VLS_ModelProps = {
58
64
  * extension, MIME type, kind, size, duration, dimensions. Output only.
59
65
  */
60
66
  'meta'?: Record<string, MediaFileMeta>;
67
+ /**
68
+ * With `profiles`: files in the grid that no role holds (replaced out of
69
+ * their last role, or unchecked). They are not in `update:profiles`, so the
70
+ * page's save drops them; warn before saving. Set [] to clear.
71
+ */
72
+ 'unplaced'?: string[];
61
73
  };
62
74
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
63
75
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
@@ -66,14 +78,18 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
66
78
  "update:modelValue": (value: string[]) => any;
67
79
  "update:alt": (value: Record<string, string> | undefined) => any;
68
80
  "update:meta": (value: Record<string, MediaFileMeta>) => any;
81
+ picked: (assetIds: string[], role: string) => any;
69
82
  "update:profiles": (profiles: MediaGalleryProfiles) => any;
83
+ "update:unplaced": (value: string[]) => any;
70
84
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
71
85
  onUploaded?: ((assetId: string, role: string, sessionId: string | null) => any) | undefined;
72
86
  onMeta?: ((assetId: string, meta: MediaFileMeta | null) => any) | undefined;
73
87
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
74
88
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
75
89
  "onUpdate:meta"?: ((value: Record<string, MediaFileMeta>) => any) | undefined;
90
+ onPicked?: ((assetIds: string[], role: string) => any) | undefined;
76
91
  "onUpdate:profiles"?: ((profiles: MediaGalleryProfiles) => any) | undefined;
92
+ "onUpdate:unplaced"?: ((value: string[]) => any) | undefined;
77
93
  }>, {
78
94
  title: string;
79
95
  visibility: "private" | "restricted" | "public";
@@ -91,9 +107,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
91
107
  accept: readonly string[] | string;
92
108
  max: number | null;
93
109
  maxSize: number | null;
110
+ pickFrom: readonly string[] | null;
111
+ onPick: () => void;
94
112
  hint: string;
95
113
  picker: boolean;
96
- onPick: () => void;
97
114
  editor: boolean;
98
115
  onOpen: (assetId: string) => void;
99
116
  featured: boolean;