nuxt-media-library 0.0.64 → 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 (26) 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 +1 -1
  6. package/dist/runtime/components/MLMedia/Field.vue.d.ts +1 -1
  7. package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +1 -1
  8. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +33 -287
  9. package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +1 -1
  10. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +1 -1
  11. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +1 -1
  12. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +1 -1
  13. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +1 -1
  14. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +1 -1
  15. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +1 -1
  16. package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +7 -0
  17. package/dist/runtime/components/MLMedia/PickerCard.vue +17 -1
  18. package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +7 -0
  19. package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +16 -1
  20. package/dist/runtime/components/MLMedia/PickerRow.vue +47 -30
  21. package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +16 -1
  22. package/dist/runtime/components/MLProcessing/StepLayout.d.vue.ts +1 -1
  23. package/dist/runtime/components/MLProcessing/StepLayout.vue.d.ts +1 -1
  24. package/dist/runtime/utils/browserActions.d.ts +47 -0
  25. package/dist/runtime/utils/browserActions.js +36 -0
  26. package/package.json +1 -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
+ };
@@ -86,10 +86,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
86
86
  pickFrom: readonly string[] | null;
87
87
  required: boolean;
88
88
  removeLabel: string;
89
+ onPick: () => void;
89
90
  hint: string;
90
91
  missingAlt: boolean;
91
92
  picker: boolean;
92
- onPick: () => void;
93
93
  editor: boolean;
94
94
  onOpen: (assetId: string) => void;
95
95
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -86,10 +86,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
86
86
  pickFrom: readonly string[] | null;
87
87
  required: boolean;
88
88
  removeLabel: string;
89
+ onPick: () => void;
89
90
  hint: string;
90
91
  missingAlt: boolean;
91
92
  picker: boolean;
92
- onPick: () => void;
93
93
  editor: boolean;
94
94
  onOpen: (assetId: string) => void;
95
95
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -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;
@@ -1,13 +1,6 @@
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 },
@@ -20,291 +13,44 @@ const props = defineProps({
20
13
  bodyClass: { type: String, required: false, default: "max-h-[min(60vh,560px)]" }
21
14
  });
22
15
  const emit = defineEmits(["done", "cancel"]);
23
- const acceptKinds = computed(() => {
24
- const raw = typeof props.accept === "string" ? props.accept.split(",") : [...props.accept];
25
- return raw.map((k) => k.trim().toLowerCase()).filter(Boolean);
26
- });
27
- const isAccepted = (kind) => !acceptKinds.value.length || acceptKinds.value.includes((kind ?? "").toLowerCase());
28
- const notAcceptedReason = computed(() => `Only ${describeKinds(acceptKinds.value)} can be chosen here`);
29
- const search = ref("");
30
- const view = ref("grid");
31
- const kindFilter = ref(acceptKinds.value.length === 1 ? [acceptKinds.value[0]] : []);
32
- const kindOptions = computed(
33
- () => (acceptKinds.value.length ? acceptKinds.value : ALL_KINDS).map((k) => ({ label: assetKindMeta(k).label, value: k }))
34
- );
35
- const filtering = computed(() => !!search.value.trim() || kindFilter.value.length > 0);
36
- const browse = useMLMediaBrowse(
37
- () => ({ kind: kindFilter.value[0] ?? null, query: search.value }),
38
- props.pickFrom ? { ids: () => props.pickFrom ?? [], backend: () => props.upload?.backend } : {}
39
- );
40
16
  const initial = [...props.selected];
41
17
  const chosen = ref([...initial]);
42
- const limitNote = ref(null);
43
- const single = computed(() => props.max === 1);
44
18
  const changed = computed(() => chosen.value.length !== initial.length || chosen.value.some((id, i) => id !== initial[i]));
45
- function isChosen(id) {
46
- return !!id && chosen.value.includes(id);
47
- }
48
- function choose(id) {
49
- limitNote.value = null;
50
- if (single.value) {
51
- chosen.value = [id];
52
- return true;
53
- }
54
- if (chosen.value.length >= props.max) {
55
- limitNote.value = `You can choose up to ${props.max}. Unselect one first.`;
56
- return false;
57
- }
58
- chosen.value = [...chosen.value, id];
59
- return true;
60
- }
61
- function toggle(id) {
62
- if (isChosen(id)) {
63
- limitNote.value = null;
64
- chosen.value = chosen.value.filter((x) => x !== id);
65
- } else {
66
- choose(id);
67
- }
68
- }
69
- const pinned = useMLMediaAssets(() => initial, { backend: () => props.upload?.backend });
70
- const pinnedAssets = computed(() => initial.map((id) => pinned.get(id)).filter((a) => !!a));
71
- const uploader = useMLMediaUpload(props.upload);
72
- const rejectNote = ref(null);
73
- function onFiles(files) {
74
- rejectNote.value = null;
75
- for (const file of files) {
76
- const u = uploader.start(file);
77
- void u.created.then((id) => {
78
- if (!id) return;
79
- u.attached = true;
80
- props.onUploaded?.(id, u.sessionId);
81
- if (!isChosen(id)) choose(id);
82
- });
83
- }
84
- }
85
- function onRejected(rejected) {
86
- const first = rejected[0];
87
- if (!first) return;
88
- const more = rejected.length > 1 ? ` (and ${rejected.length - 1} more)` : "";
89
- 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"}.`;
90
- }
91
- const uploadIds = computed(() => new Set(uploader.uploads.value.map((u) => u.assetId).filter(Boolean)));
92
- const visibleUploads = computed(() => uploader.uploads.value.filter((u) => u.phase !== "canceled"));
93
- function uploadPhase(u) {
94
- return u.phase === "uploading" || u.phase === "processing" || u.phase === "failed" ? u.phase : null;
95
- }
96
- const listed = computed(() => {
97
- const pinnedIds = new Set(filtering.value ? [] : pinnedAssets.value.map((a) => a.id));
98
- const rest = browse.items.value.filter((a) => !pinnedIds.has(a.id) && !uploadIds.value.has(a.id));
99
- return filtering.value ? rest : [...pinnedAssets.value, ...rest];
100
- });
101
- const initialLoading = computed(() => browse.loading.value && !browse.items.value.length);
102
- const isEmpty = computed(() => !initialLoading.value && !listed.value.length && !visibleUploads.value.length);
103
- watch(kindFilter, () => {
104
- limitNote.value = null;
105
- });
106
- const mediaEditor = useMLMediaEditor();
107
- function preview(id) {
108
- void mediaEditor.open(id, { readonly: true, siblings: listed.value.map((a) => a.id) });
109
- }
110
- const footerText = computed(() => {
111
- if (limitNote.value) return limitNote.value;
112
- const n = chosen.value.length;
113
- if (single.value) return n ? "1 selected" : "Nothing selected";
114
- return Number.isFinite(props.max) ? `${n} of ${props.max} selected` : `${n} selected`;
115
- });
116
19
  </script>
117
20
 
118
21
  <template>
119
- <div class="flex min-h-0 flex-col">
120
- <div class="flex flex-col gap-3 border-b border-default p-4">
121
- <div class="flex flex-wrap items-center gap-2">
122
- <UInput
123
- v-model="search"
124
- icon="i-lucide-search"
125
- placeholder="Search files"
126
- class="min-w-0 flex-1 basis-56"
127
- :loading="browse.loading.value && !!search"
128
- 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')"
129
47
  />
130
- <UFieldGroup size="md">
131
- <UButton
132
- icon="i-lucide-layout-grid"
133
- color="neutral"
134
- :variant="view === 'grid' ? 'soft' : 'outline'"
135
- aria-label="Grid view"
136
- :aria-pressed="view === 'grid'"
137
- @click="view = 'grid'"
138
- />
139
- <UButton
140
- icon="i-lucide-list"
141
- color="neutral"
142
- :variant="view === 'list' ? 'soft' : 'outline'"
143
- aria-label="List view"
144
- :aria-pressed="view === 'list'"
145
- @click="view = 'list'"
146
- />
147
- </UFieldGroup>
148
- </div>
149
- <div
150
- v-if="kindOptions.length > 1"
151
- class="flex flex-wrap gap-1.5"
152
- >
153
- <MLMediaFilterPill
154
- v-model="kindFilter"
155
- label="File type"
156
- :options="kindOptions"
157
- single
48
+ <UButton
49
+ label="Done"
50
+ :disabled="!changed"
51
+ @click="emit('done', [...chosen])"
158
52
  />
159
53
  </div>
160
- </div>
161
-
162
- <div
163
- class="flex min-h-0 flex-col gap-3 overflow-y-auto p-4"
164
- :class="bodyClass"
165
- >
166
- <MLMediaDropzone
167
- :accept="acceptForKinds(acceptKinds)"
168
- :multiple="!single"
169
- :max-size="maxSize"
170
- compact
171
- :title="single ? 'Drop a file to add it to the library' : 'Drop files to add them to the library'"
172
- button-label="Upload from computer"
173
- @files="onFiles"
174
- @rejected="onRejected"
175
- />
176
- <p
177
- v-if="rejectNote"
178
- class="text-xs text-error"
179
- >
180
- {{ rejectNote }}
181
- </p>
182
-
183
- <UAlert
184
- v-if="browse.error.value"
185
- color="error"
186
- variant="subtle"
187
- icon="i-lucide-circle-alert"
188
- :title="browse.error.value"
189
- :actions="[{ label: 'Try again', color: 'neutral', variant: 'outline', onClick: () => browse.reload() }]"
190
- />
191
-
192
- <!-- Loading -->
193
- <div
194
- v-if="initialLoading"
195
- class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
196
- >
197
- <div
198
- v-for="i in 12"
199
- :key="i"
200
- class="flex flex-col items-center gap-2 p-2.5"
201
- >
202
- <USkeleton class="aspect-square w-full max-w-28 rounded-md" />
203
- <USkeleton class="h-3 w-3/4" />
204
- </div>
205
- </div>
206
-
207
- <!-- Empty -->
208
- <UEmpty
209
- v-else-if="isEmpty && !browse.error.value"
210
- variant="naked"
211
- :icon="filtering ? 'i-lucide-search-x' : 'i-lucide-folder-open'"
212
- :title="filtering ? 'No files match' : 'No files yet'"
213
- :description="filtering ? 'Try another search or clear the file type.' : 'Drop files above to add the first one.'"
214
- />
215
-
216
- <!-- Grid -->
217
- <div
218
- v-else-if="view === 'grid'"
219
- class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
220
- >
221
- <MLMediaPickerCard
222
- v-for="u in visibleUploads"
223
- :key="u.key"
224
- :src="u.localPreview"
225
- :kind="u.kind"
226
- :name="u.file.name"
227
- :meta="u.phase === 'failed' ? u.error ?? 'Failed' : void 0"
228
- :selected="isChosen(u.assetId)"
229
- :disabled="!u.assetId || !isAccepted(u.kind)"
230
- @toggle="u.assetId && toggle(u.assetId)"
231
- >
232
- <MLMediaProgressVeil
233
- v-if="uploadPhase(u)"
234
- :phase="uploadPhase(u)"
235
- :progress="u.progress"
236
- compact
237
- />
238
- </MLMediaPickerCard>
239
- <MLMediaPickerCard
240
- v-for="a in listed"
241
- :key="a.id"
242
- :asset="a"
243
- :selected="isChosen(a.id)"
244
- :disabled="!isAccepted(a.kind) || a.status === 'failed'"
245
- :disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
246
- :on-preview="() => preview(a.id)"
247
- @toggle="toggle(a.id)"
248
- />
249
- </div>
250
-
251
- <!-- List -->
252
- <div
253
- v-else
254
- class="flex flex-col divide-y divide-default overflow-hidden rounded-lg border border-default"
255
- >
256
- <MLMediaPickerRow
257
- v-for="u in visibleUploads"
258
- :key="u.key"
259
- :src="u.localPreview"
260
- :kind="u.kind"
261
- :name="u.file.name"
262
- :size="u.phase === 'uploading' ? `${Math.round(u.progress ?? 0)}%` : formatBytes(u.file.size)"
263
- :date="u.phase === 'ready' ? 'Just now' : u.phase === 'failed' ? 'Failed' : 'Uploading'"
264
- :selected="isChosen(u.assetId)"
265
- :disabled="!u.assetId || !isAccepted(u.kind)"
266
- @toggle="u.assetId && toggle(u.assetId)"
267
- />
268
- <MLMediaPickerRow
269
- v-for="a in listed"
270
- :key="a.id"
271
- :asset="a"
272
- :selected="isChosen(a.id)"
273
- :disabled="!isAccepted(a.kind) || a.status === 'failed'"
274
- :disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
275
- @toggle="toggle(a.id)"
276
- />
277
- </div>
278
-
279
- <UButton
280
- v-if="browse.hasMore.value && !initialLoading"
281
- class="self-center"
282
- color="neutral"
283
- variant="outline"
284
- size="sm"
285
- label="Load more"
286
- :loading="browse.loading.value"
287
- @click="browse.loadMore()"
288
- />
289
- </div>
290
-
291
- <div class="flex flex-wrap items-center gap-2 border-t border-default px-4 py-3">
292
- <span
293
- class="me-auto text-sm"
294
- :class="limitNote ? 'text-warning' : 'text-muted'"
295
- aria-live="polite"
296
- >{{ footerText }}</span>
297
- <UButton
298
- color="neutral"
299
- variant="outline"
300
- label="Cancel"
301
- @click="emit('cancel')"
302
- />
303
- <UButton
304
- label="Done"
305
- :disabled="!changed"
306
- @click="emit('done', [...chosen])"
307
- />
308
- </div>
309
- </div>
54
+ </template>
55
+ </MLMediaBrowser>
310
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;
@@ -108,9 +108,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
108
108
  max: number | null;
109
109
  maxSize: number | null;
110
110
  pickFrom: readonly string[] | null;
111
+ onPick: () => void;
111
112
  hint: string;
112
113
  picker: boolean;
113
- onPick: () => void;
114
114
  editor: boolean;
115
115
  onOpen: (assetId: string) => void;
116
116
  featured: boolean;
@@ -108,9 +108,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
108
108
  max: number | null;
109
109
  maxSize: number | null;
110
110
  pickFrom: readonly string[] | null;
111
+ onPick: () => void;
111
112
  hint: string;
112
113
  picker: boolean;
113
- onPick: () => void;
114
114
  editor: boolean;
115
115
  onOpen: (assetId: string) => void;
116
116
  featured: boolean;
@@ -104,9 +104,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
104
104
  pickFrom: readonly string[] | null;
105
105
  removeLabel: string;
106
106
  onAdd: (assetId: string, tag: string | undefined) => void;
107
+ onPick: () => void;
107
108
  hint: string;
108
109
  picker: boolean;
109
- onPick: () => void;
110
110
  editor: boolean;
111
111
  onOpen: (assetId: string) => void;
112
112
  featured: boolean;
@@ -104,9 +104,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
104
104
  pickFrom: readonly string[] | null;
105
105
  removeLabel: string;
106
106
  onAdd: (assetId: string, tag: string | undefined) => void;
107
+ onPick: () => void;
107
108
  hint: string;
108
109
  picker: boolean;
109
- onPick: () => void;
110
110
  editor: boolean;
111
111
  onOpen: (assetId: string) => void;
112
112
  featured: boolean;
@@ -46,9 +46,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
46
46
  disabled: boolean;
47
47
  phase: ProgressVeilPhase | null;
48
48
  selected: boolean;
49
+ selectable: boolean;
49
50
  missingAlt: boolean;
50
51
  onOpen: () => void;
51
- selectable: boolean;
52
52
  selecting: boolean;
53
53
  phaseDetail: string;
54
54
  aspect: "square" | "landscape" | "portrait" | "video";
@@ -46,9 +46,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
46
46
  disabled: boolean;
47
47
  phase: ProgressVeilPhase | null;
48
48
  selected: boolean;
49
+ selectable: boolean;
49
50
  missingAlt: boolean;
50
51
  onOpen: () => void;
51
- selectable: boolean;
52
52
  selecting: boolean;
53
53
  phaseDetail: string;
54
54
  aspect: "square" | "landscape" | "portrait" | "video";
@@ -7,6 +7,8 @@ type __VLS_Props = {
7
7
  /** Second line. Defaults to the file extension. */
8
8
  meta?: string;
9
9
  selected?: boolean;
10
+ /** Shows the checkbox. False for a list without selection. */
11
+ selectable?: boolean;
10
12
  /** Dimmed and not selectable, e.g. a kind the calling field doesn't accept. */
11
13
  disabled?: boolean;
12
14
  disabledReason?: string;
@@ -16,6 +18,10 @@ type __VLS_Props = {
16
18
  type __VLS_Slots = {
17
19
  /** Overlays on the thumbnail, e.g. an upload veil. */
18
20
  default?: () => unknown;
21
+ /** Small labels over the top of the card, e.g. "Used in 3 lessons". */
22
+ badge?: () => unknown;
23
+ /** Controls in the top-right corner, e.g. a menu. Usable when the card is disabled. */
24
+ actions?: () => unknown;
19
25
  };
20
26
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
21
27
  toggle: () => any;
@@ -29,6 +35,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
29
35
  disabled: boolean;
30
36
  meta: string;
31
37
  selected: boolean;
38
+ selectable: boolean;
32
39
  disabledReason: string;
33
40
  onPreview: () => void;
34
41
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -8,6 +8,7 @@ const props = defineProps({
8
8
  name: { type: String, required: false, default: void 0 },
9
9
  meta: { type: String, required: false, default: void 0 },
10
10
  selected: { type: Boolean, required: false, default: false },
11
+ selectable: { type: Boolean, required: false, default: true },
11
12
  disabled: { type: Boolean, required: false, default: false },
12
13
  disabledReason: { type: String, required: false, default: void 0 },
13
14
  onPreview: { type: Function, required: false, default: void 0 }
@@ -30,7 +31,7 @@ const isTall = computed(() => (resolvedKind.value ?? "").toLowerCase() === "docu
30
31
  type="button"
31
32
  class="flex w-full flex-col items-center gap-2 rounded-lg px-2.5 pb-2.5 pt-3 text-center outline-offset-2 focus-visible:outline-2 focus-visible:outline-primary"
32
33
  :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
33
- :aria-pressed="selected"
34
+ :aria-pressed="selectable ? selected : void 0"
34
35
  :disabled="disabled"
35
36
  @click="emit('toggle')"
36
37
  >
@@ -58,6 +59,7 @@ const isTall = computed(() => (resolvedKind.value ?? "").toLowerCase() === "docu
58
59
  </button>
59
60
 
60
61
  <span
62
+ v-if="selectable"
61
63
  class="pointer-events-none absolute left-2 top-2 grid size-4 place-items-center rounded border-[1.5px]"
62
64
  :class="selected ? 'border-inverted bg-inverted text-inverted' : 'border-accented bg-default'"
63
65
  aria-hidden="true"
@@ -69,6 +71,20 @@ const isTall = computed(() => (resolvedKind.value ?? "").toLowerCase() === "docu
69
71
  />
70
72
  </span>
71
73
 
74
+ <div
75
+ v-if="$slots.badge"
76
+ class="pointer-events-none absolute right-9 top-2 flex flex-wrap gap-1"
77
+ :class="selectable ? 'left-8' : 'left-2'"
78
+ >
79
+ <slot name="badge" />
80
+ </div>
81
+ <div
82
+ v-if="$slots.actions"
83
+ class="absolute right-1.5 top-1.5 z-10 flex items-center gap-1"
84
+ >
85
+ <slot name="actions" />
86
+ </div>
87
+
72
88
  <MLMediaPreviewEye
73
89
  v-if="onPreview && !disabled"
74
90
  :name="resolvedName"
@@ -7,6 +7,8 @@ type __VLS_Props = {
7
7
  /** Second line. Defaults to the file extension. */
8
8
  meta?: string;
9
9
  selected?: boolean;
10
+ /** Shows the checkbox. False for a list without selection. */
11
+ selectable?: boolean;
10
12
  /** Dimmed and not selectable, e.g. a kind the calling field doesn't accept. */
11
13
  disabled?: boolean;
12
14
  disabledReason?: string;
@@ -16,6 +18,10 @@ type __VLS_Props = {
16
18
  type __VLS_Slots = {
17
19
  /** Overlays on the thumbnail, e.g. an upload veil. */
18
20
  default?: () => unknown;
21
+ /** Small labels over the top of the card, e.g. "Used in 3 lessons". */
22
+ badge?: () => unknown;
23
+ /** Controls in the top-right corner, e.g. a menu. Usable when the card is disabled. */
24
+ actions?: () => unknown;
19
25
  };
20
26
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
21
27
  toggle: () => any;
@@ -29,6 +35,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
29
35
  disabled: boolean;
30
36
  meta: string;
31
37
  selected: boolean;
38
+ selectable: boolean;
32
39
  disabledReason: string;
33
40
  onPreview: () => void;
34
41
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;