nuxt-media-library 0.0.48 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/components/MLMedia/Dropzone.d.vue.ts +60 -0
- package/dist/runtime/components/MLMedia/Dropzone.vue +137 -0
- package/dist/runtime/components/MLMedia/Dropzone.vue.d.ts +60 -0
- package/dist/runtime/components/MLMedia/Editor.d.vue.ts +23 -0
- package/dist/runtime/components/MLMedia/Editor.vue +34 -0
- package/dist/runtime/components/MLMedia/Editor.vue.d.ts +23 -0
- package/dist/runtime/components/MLMedia/EditorPanel.d.vue.ts +38 -0
- package/dist/runtime/components/MLMedia/EditorPanel.vue +220 -0
- package/dist/runtime/components/MLMedia/EditorPanel.vue.d.ts +38 -0
- package/dist/runtime/components/MLMedia/Field.d.vue.ts +65 -0
- package/dist/runtime/components/MLMedia/Field.vue +282 -0
- package/dist/runtime/components/MLMedia/Field.vue.d.ts +65 -0
- package/dist/runtime/components/MLMedia/FileFacts.d.vue.ts +19 -0
- package/dist/runtime/components/MLMedia/FileFacts.vue +38 -0
- package/dist/runtime/components/MLMedia/FileFacts.vue.d.ts +19 -0
- package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +23 -0
- package/dist/runtime/components/MLMedia/FilePicker.vue +33 -0
- package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +23 -0
- package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +33 -0
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue +300 -0
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +33 -0
- package/dist/runtime/components/MLMedia/FilterPill.d.vue.ts +30 -0
- package/dist/runtime/components/MLMedia/FilterPill.vue +84 -0
- package/dist/runtime/components/MLMedia/FilterPill.vue.d.ts +30 -0
- package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +78 -0
- package/dist/runtime/components/MLMedia/Gallery.vue +256 -0
- package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +78 -0
- package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +102 -0
- package/dist/runtime/components/MLMedia/GallerySection.vue +422 -0
- package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +102 -0
- package/dist/runtime/components/MLMedia/KindBadge.d.vue.ts +17 -0
- package/dist/runtime/components/MLMedia/KindBadge.vue +22 -0
- package/dist/runtime/components/MLMedia/KindBadge.vue.d.ts +17 -0
- package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +63 -0
- package/dist/runtime/components/MLMedia/MediaTile.vue +117 -0
- package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +63 -0
- package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +42 -0
- package/dist/runtime/components/MLMedia/PickerCard.vue +79 -0
- package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +42 -0
- package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +31 -0
- package/dist/runtime/components/MLMedia/PickerRow.vue +65 -0
- package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +31 -0
- package/dist/runtime/components/MLMedia/PreviewEye.d.vue.ts +16 -0
- package/dist/runtime/components/MLMedia/PreviewEye.vue +21 -0
- package/dist/runtime/components/MLMedia/PreviewEye.vue.d.ts +16 -0
- package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +25 -0
- package/dist/runtime/components/MLMedia/ProgressVeil.vue +98 -0
- package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +25 -0
- package/dist/runtime/components/MLMedia/StatusBadge.d.vue.ts +17 -0
- package/dist/runtime/components/MLMedia/StatusBadge.vue +31 -0
- package/dist/runtime/components/MLMedia/StatusBadge.vue.d.ts +17 -0
- package/dist/runtime/components/MLMedia/Thumbnail.d.vue.ts +36 -0
- package/dist/runtime/components/MLMedia/Thumbnail.vue +84 -0
- package/dist/runtime/components/MLMedia/Thumbnail.vue.d.ts +36 -0
- package/dist/runtime/components/MLMedia/UsageList.d.vue.ts +26 -0
- package/dist/runtime/components/MLMedia/UsageList.vue +66 -0
- package/dist/runtime/components/MLMedia/UsageList.vue.d.ts +26 -0
- package/dist/runtime/components/MLProcessing/StepLayout.d.vue.ts +1 -1
- package/dist/runtime/components/MLProcessing/StepLayout.vue.d.ts +1 -1
- package/dist/runtime/composables/useMLFilePicker.d.ts +20 -0
- package/dist/runtime/composables/useMLFilePicker.js +17 -0
- package/dist/runtime/composables/useMLMediaAssets.d.ts +19 -0
- package/dist/runtime/composables/useMLMediaAssets.js +56 -0
- package/dist/runtime/composables/useMLMediaBrowse.d.ts +147 -0
- package/dist/runtime/composables/useMLMediaBrowse.js +72 -0
- package/dist/runtime/composables/useMLMediaEditor.d.ts +22 -0
- package/dist/runtime/composables/useMLMediaEditor.js +20 -0
- package/dist/runtime/composables/useMLMediaField.d.ts +29 -0
- package/dist/runtime/composables/useMLMediaField.js +77 -0
- package/dist/runtime/composables/useMLMediaGallery.d.ts +55 -0
- package/dist/runtime/composables/useMLMediaGallery.js +134 -0
- package/dist/runtime/composables/useMLMediaUpload.d.ts +134 -0
- package/dist/runtime/composables/useMLMediaUpload.js +153 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.js +11 -0
- package/dist/runtime/utils/assetDisplay.d.ts +8 -0
- package/dist/runtime/utils/assetDisplay.js +29 -0
- package/dist/runtime/utils/assetFormat.d.ts +30 -0
- package/dist/runtime/utils/assetFormat.js +69 -0
- package/dist/runtime/utils/assetThumbnail.d.ts +11 -0
- package/dist/runtime/utils/assetThumbnail.js +14 -0
- package/dist/runtime/utils/fileAccept.d.ts +26 -0
- package/dist/runtime/utils/fileAccept.js +104 -0
- package/dist/runtime/utils/galleryDrag.d.ts +2 -0
- package/dist/runtime/utils/galleryDrag.js +1 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type RejectedFile } from '../../utils/fileAccept.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Browser accept syntax ("image/*,.glb"). Build one from kinds with acceptForKinds(). */
|
|
4
|
+
accept?: string;
|
|
5
|
+
multiple?: boolean;
|
|
6
|
+
/** Bytes. Larger files are rejected with reason "size". */
|
|
7
|
+
maxSize?: number | null;
|
|
8
|
+
/** How many more files fit. Extra files are rejected with reason "count". */
|
|
9
|
+
maxFiles?: number | null;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* `area`: a dashed box with an upload button (fields, picker).
|
|
13
|
+
* `overlay`: wraps the default slot and only shows a highlight while files are dragged over it (gallery).
|
|
14
|
+
*/
|
|
15
|
+
variant?: 'area' | 'overlay';
|
|
16
|
+
title?: string;
|
|
17
|
+
hint?: string;
|
|
18
|
+
/** The button that opens the computer's file dialog. Defaults to "Upload file(s)". */
|
|
19
|
+
buttonLabel?: string;
|
|
20
|
+
/** One short row instead of a box, for places where the drop zone is secondary (the picker). */
|
|
21
|
+
compact?: boolean;
|
|
22
|
+
};
|
|
23
|
+
type __VLS_Slots = {
|
|
24
|
+
default?: (props: {
|
|
25
|
+
isDragging: boolean;
|
|
26
|
+
open: () => void;
|
|
27
|
+
}) => unknown;
|
|
28
|
+
actions?: (props: {
|
|
29
|
+
open: () => void;
|
|
30
|
+
}) => unknown;
|
|
31
|
+
};
|
|
32
|
+
declare function open(): void;
|
|
33
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
34
|
+
open: typeof open;
|
|
35
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
36
|
+
files: (files: File[]) => any;
|
|
37
|
+
rejected: (files: RejectedFile[]) => any;
|
|
38
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
39
|
+
onFiles?: ((files: File[]) => any) | undefined;
|
|
40
|
+
onRejected?: ((files: RejectedFile[]) => any) | undefined;
|
|
41
|
+
}>, {
|
|
42
|
+
title: string;
|
|
43
|
+
accept: string;
|
|
44
|
+
maxSize: number | null;
|
|
45
|
+
variant: "area" | "overlay";
|
|
46
|
+
compact: boolean;
|
|
47
|
+
disabled: boolean;
|
|
48
|
+
multiple: boolean;
|
|
49
|
+
maxFiles: number | null;
|
|
50
|
+
hint: string;
|
|
51
|
+
buttonLabel: string;
|
|
52
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
53
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
54
|
+
declare const _default: typeof __VLS_export;
|
|
55
|
+
export default _default;
|
|
56
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
57
|
+
new (): {
|
|
58
|
+
$slots: S;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import { useFileUpload } from "#imports";
|
|
4
|
+
import { formatBytes } from "../../utils/assetFormat";
|
|
5
|
+
import { partitionFiles } from "../../utils/fileAccept";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
accept: { type: String, required: false, default: "*" },
|
|
8
|
+
multiple: { type: Boolean, required: false, default: true },
|
|
9
|
+
maxSize: { type: [Number, null], required: false, default: null },
|
|
10
|
+
maxFiles: { type: [Number, null], required: false, default: null },
|
|
11
|
+
disabled: { type: Boolean, required: false, default: false },
|
|
12
|
+
variant: { type: String, required: false, default: "area" },
|
|
13
|
+
title: { type: String, required: false, default: void 0 },
|
|
14
|
+
hint: { type: String, required: false, default: void 0 },
|
|
15
|
+
buttonLabel: { type: String, required: false, default: void 0 },
|
|
16
|
+
compact: { type: Boolean, required: false, default: false }
|
|
17
|
+
});
|
|
18
|
+
const emit = defineEmits(["files", "rejected"]);
|
|
19
|
+
defineSlots();
|
|
20
|
+
const { isDragging, dropzoneRef } = useFileUpload({
|
|
21
|
+
accept: "*",
|
|
22
|
+
multiple: true,
|
|
23
|
+
dropzone: true,
|
|
24
|
+
onUpdate: (files) => handle(files)
|
|
25
|
+
});
|
|
26
|
+
const input = ref();
|
|
27
|
+
function handle(files) {
|
|
28
|
+
if (props.disabled || !files.length) return;
|
|
29
|
+
const max = props.multiple ? props.maxFiles : Math.min(1, props.maxFiles ?? 1);
|
|
30
|
+
const { accepted, rejected } = partitionFiles(files, { accept: props.accept, maxSize: props.maxSize, maxFiles: max });
|
|
31
|
+
if (accepted.length) emit("files", accepted);
|
|
32
|
+
if (rejected.length) emit("rejected", rejected);
|
|
33
|
+
}
|
|
34
|
+
function open() {
|
|
35
|
+
if (!props.disabled) input.value?.click();
|
|
36
|
+
}
|
|
37
|
+
function onInput(e) {
|
|
38
|
+
const el = e.target;
|
|
39
|
+
handle(Array.from(el.files ?? []));
|
|
40
|
+
el.value = "";
|
|
41
|
+
}
|
|
42
|
+
const carriesFiles = ref(false);
|
|
43
|
+
function onDragEnter(e) {
|
|
44
|
+
carriesFiles.value = !!e.dataTransfer && Array.from(e.dataTransfer.types).includes("Files");
|
|
45
|
+
}
|
|
46
|
+
const active = computed(() => isDragging.value && carriesFiles.value && !props.disabled);
|
|
47
|
+
const sizeHint = computed(() => props.maxSize ? `Up to ${formatBytes(props.maxSize)}` : "");
|
|
48
|
+
defineExpose({ open });
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div
|
|
53
|
+
ref="dropzoneRef"
|
|
54
|
+
class="relative"
|
|
55
|
+
@dragenter.capture="onDragEnter"
|
|
56
|
+
:data-dragging="active || void 0"
|
|
57
|
+
:aria-disabled="disabled || void 0"
|
|
58
|
+
>
|
|
59
|
+
<input
|
|
60
|
+
ref="input"
|
|
61
|
+
type="file"
|
|
62
|
+
class="sr-only"
|
|
63
|
+
tabindex="-1"
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
:accept="accept === '*' ? void 0 : accept"
|
|
66
|
+
:multiple="multiple"
|
|
67
|
+
:disabled="disabled"
|
|
68
|
+
@change="onInput"
|
|
69
|
+
>
|
|
70
|
+
|
|
71
|
+
<template v-if="variant === 'overlay'">
|
|
72
|
+
<slot
|
|
73
|
+
:is-dragging="active"
|
|
74
|
+
:open="open"
|
|
75
|
+
/>
|
|
76
|
+
<div
|
|
77
|
+
v-if="active"
|
|
78
|
+
class="pointer-events-none absolute inset-0 z-20 grid place-items-center rounded-[inherit] border-2 border-dashed border-primary bg-primary/10 backdrop-blur-[1px]"
|
|
79
|
+
>
|
|
80
|
+
<span class="flex items-center gap-2 rounded-md bg-default px-3 py-1.5 text-sm font-semibold text-primary shadow-sm">
|
|
81
|
+
<UIcon
|
|
82
|
+
name="i-lucide-upload"
|
|
83
|
+
class="size-4"
|
|
84
|
+
/>
|
|
85
|
+
{{ title ?? "Drop to upload" }}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<div
|
|
91
|
+
v-else
|
|
92
|
+
class="flex items-center rounded-lg border-[1.5px] border-dashed text-center transition-colors motion-reduce:transition-none"
|
|
93
|
+
:class="[
|
|
94
|
+
compact ? 'flex-row flex-wrap justify-center gap-x-3 gap-y-1.5 px-3 py-2.5' : 'flex-col gap-2.5 px-4 py-5',
|
|
95
|
+
active ? 'border-primary bg-primary/10' : 'border-accented bg-default',
|
|
96
|
+
disabled ? 'opacity-60' : ''
|
|
97
|
+
]"
|
|
98
|
+
>
|
|
99
|
+
<slot
|
|
100
|
+
:is-dragging="active"
|
|
101
|
+
:open="open"
|
|
102
|
+
>
|
|
103
|
+
<UIcon
|
|
104
|
+
v-if="!compact"
|
|
105
|
+
name="i-lucide-upload"
|
|
106
|
+
class="size-5"
|
|
107
|
+
:class="active ? 'text-primary' : 'text-dimmed'"
|
|
108
|
+
/>
|
|
109
|
+
<p class="text-sm">
|
|
110
|
+
<span :class="compact ? 'text-muted' : 'font-semibold'">{{ title ?? (multiple ? "Drop files here" : "Drop a file here") }}</span>
|
|
111
|
+
<span class="text-muted"> or</span>
|
|
112
|
+
</p>
|
|
113
|
+
<div class="flex flex-wrap justify-center gap-1.5">
|
|
114
|
+
<UButton
|
|
115
|
+
size="sm"
|
|
116
|
+
color="neutral"
|
|
117
|
+
variant="outline"
|
|
118
|
+
icon="i-lucide-upload"
|
|
119
|
+
:label="buttonLabel ?? (multiple ? 'Upload files' : 'Upload file')"
|
|
120
|
+
:disabled="disabled"
|
|
121
|
+
@click="open"
|
|
122
|
+
/>
|
|
123
|
+
<slot
|
|
124
|
+
name="actions"
|
|
125
|
+
:open="open"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<p
|
|
129
|
+
v-if="hint || sizeHint"
|
|
130
|
+
class="text-xs text-dimmed"
|
|
131
|
+
>
|
|
132
|
+
{{ [hint, sizeHint].filter(Boolean).join(" \xB7 ") }}
|
|
133
|
+
</p>
|
|
134
|
+
</slot>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type RejectedFile } from '../../utils/fileAccept.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** Browser accept syntax ("image/*,.glb"). Build one from kinds with acceptForKinds(). */
|
|
4
|
+
accept?: string;
|
|
5
|
+
multiple?: boolean;
|
|
6
|
+
/** Bytes. Larger files are rejected with reason "size". */
|
|
7
|
+
maxSize?: number | null;
|
|
8
|
+
/** How many more files fit. Extra files are rejected with reason "count". */
|
|
9
|
+
maxFiles?: number | null;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* `area`: a dashed box with an upload button (fields, picker).
|
|
13
|
+
* `overlay`: wraps the default slot and only shows a highlight while files are dragged over it (gallery).
|
|
14
|
+
*/
|
|
15
|
+
variant?: 'area' | 'overlay';
|
|
16
|
+
title?: string;
|
|
17
|
+
hint?: string;
|
|
18
|
+
/** The button that opens the computer's file dialog. Defaults to "Upload file(s)". */
|
|
19
|
+
buttonLabel?: string;
|
|
20
|
+
/** One short row instead of a box, for places where the drop zone is secondary (the picker). */
|
|
21
|
+
compact?: boolean;
|
|
22
|
+
};
|
|
23
|
+
type __VLS_Slots = {
|
|
24
|
+
default?: (props: {
|
|
25
|
+
isDragging: boolean;
|
|
26
|
+
open: () => void;
|
|
27
|
+
}) => unknown;
|
|
28
|
+
actions?: (props: {
|
|
29
|
+
open: () => void;
|
|
30
|
+
}) => unknown;
|
|
31
|
+
};
|
|
32
|
+
declare function open(): void;
|
|
33
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
34
|
+
open: typeof open;
|
|
35
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
36
|
+
files: (files: File[]) => any;
|
|
37
|
+
rejected: (files: RejectedFile[]) => any;
|
|
38
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
39
|
+
onFiles?: ((files: File[]) => any) | undefined;
|
|
40
|
+
onRejected?: ((files: RejectedFile[]) => any) | undefined;
|
|
41
|
+
}>, {
|
|
42
|
+
title: string;
|
|
43
|
+
accept: string;
|
|
44
|
+
maxSize: number | null;
|
|
45
|
+
variant: "area" | "overlay";
|
|
46
|
+
compact: boolean;
|
|
47
|
+
disabled: boolean;
|
|
48
|
+
multiple: boolean;
|
|
49
|
+
maxFiles: number | null;
|
|
50
|
+
hint: string;
|
|
51
|
+
buttonLabel: string;
|
|
52
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
53
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
54
|
+
declare const _default: typeof __VLS_export;
|
|
55
|
+
export default _default;
|
|
56
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
57
|
+
new (): {
|
|
58
|
+
$slots: S;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MediaUsage } from './UsageList.vue.js';
|
|
2
|
+
export type MediaEditorResult = {
|
|
3
|
+
action: 'save';
|
|
4
|
+
alt: Record<string, string>;
|
|
5
|
+
} | {
|
|
6
|
+
action: 'remove';
|
|
7
|
+
assetId: string;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_Props = {
|
|
10
|
+
assetId: string;
|
|
11
|
+
siblings?: readonly string[];
|
|
12
|
+
alt?: Readonly<Record<string, string>>;
|
|
13
|
+
readonly?: boolean;
|
|
14
|
+
removeLabel?: string;
|
|
15
|
+
usages?: Readonly<Record<string, MediaUsage[]>>;
|
|
16
|
+
};
|
|
17
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
|
+
close: (result: MediaEditorResult | null) => any;
|
|
19
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
20
|
+
onClose?: ((result: MediaEditorResult | null) => any) | undefined;
|
|
21
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
22
|
+
declare const _default: typeof __VLS_export;
|
|
23
|
+
export default _default;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
assetId: { type: String, required: true },
|
|
4
|
+
siblings: { type: Array, required: false },
|
|
5
|
+
alt: { type: Object, required: false },
|
|
6
|
+
readonly: { type: Boolean, required: false },
|
|
7
|
+
removeLabel: { type: String, required: false },
|
|
8
|
+
usages: { type: Object, required: false }
|
|
9
|
+
});
|
|
10
|
+
const emit = defineEmits(["close"]);
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<UModal
|
|
15
|
+
:title="readonly ? 'Preview' : 'Edit media'"
|
|
16
|
+
:close="{ onClick: () => emit('close', null) }"
|
|
17
|
+
:ui="{ content: 'sm:max-w-5xl', body: 'p-0 sm:p-0' }"
|
|
18
|
+
@update:open="(open) => !open && emit('close', null)"
|
|
19
|
+
>
|
|
20
|
+
<template #body>
|
|
21
|
+
<MLMediaEditorPanel
|
|
22
|
+
:asset-id="assetId"
|
|
23
|
+
:siblings="siblings"
|
|
24
|
+
:alt="alt"
|
|
25
|
+
:readonly="readonly"
|
|
26
|
+
:remove-label="removeLabel"
|
|
27
|
+
:usages="usages"
|
|
28
|
+
@save="({ alt: a }) => emit('close', { action: 'save', alt: a })"
|
|
29
|
+
@remove="(id) => emit('close', { action: 'remove', assetId: id })"
|
|
30
|
+
@cancel="emit('close', null)"
|
|
31
|
+
/>
|
|
32
|
+
</template>
|
|
33
|
+
</UModal>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MediaUsage } from './UsageList.vue.js';
|
|
2
|
+
export type MediaEditorResult = {
|
|
3
|
+
action: 'save';
|
|
4
|
+
alt: Record<string, string>;
|
|
5
|
+
} | {
|
|
6
|
+
action: 'remove';
|
|
7
|
+
assetId: string;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_Props = {
|
|
10
|
+
assetId: string;
|
|
11
|
+
siblings?: readonly string[];
|
|
12
|
+
alt?: Readonly<Record<string, string>>;
|
|
13
|
+
readonly?: boolean;
|
|
14
|
+
removeLabel?: string;
|
|
15
|
+
usages?: Readonly<Record<string, MediaUsage[]>>;
|
|
16
|
+
};
|
|
17
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
|
+
close: (result: MediaEditorResult | null) => any;
|
|
19
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
20
|
+
onClose?: ((result: MediaEditorResult | null) => any) | undefined;
|
|
21
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
22
|
+
declare const _default: typeof __VLS_export;
|
|
23
|
+
export default _default;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MediaUsage } from './UsageList.vue.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** The file to show first. */
|
|
4
|
+
assetId: string;
|
|
5
|
+
/** The record's other files, in order. Enables previous / next. */
|
|
6
|
+
siblings?: readonly string[];
|
|
7
|
+
/** Current alt text per asset ID. Alt text lives on the host record, not the file. */
|
|
8
|
+
alt?: Readonly<Record<string, string>>;
|
|
9
|
+
/** Preview only: no alt editing, no Save. Used from the picker. */
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
/** Label for the remove button, e.g. "Remove from product". Empty hides it. */
|
|
12
|
+
removeLabel?: string;
|
|
13
|
+
/** Where each file is used, per asset ID. Omit to hide the section. */
|
|
14
|
+
usages?: Readonly<Record<string, MediaUsage[]>>;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
17
|
+
currentId: import("vue").ComputedRef<string>;
|
|
18
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
|
+
remove: (assetId: string) => any;
|
|
20
|
+
cancel: () => any;
|
|
21
|
+
save: (result: {
|
|
22
|
+
alt: Record<string, string>;
|
|
23
|
+
}) => any;
|
|
24
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
25
|
+
onRemove?: ((assetId: string) => any) | undefined;
|
|
26
|
+
onCancel?: (() => any) | undefined;
|
|
27
|
+
onSave?: ((result: {
|
|
28
|
+
alt: Record<string, string>;
|
|
29
|
+
}) => any) | undefined;
|
|
30
|
+
}>, {
|
|
31
|
+
usages: Readonly<Record<string, MediaUsage[]>>;
|
|
32
|
+
siblings: readonly string[];
|
|
33
|
+
alt: Readonly<Record<string, string>>;
|
|
34
|
+
readonly: boolean;
|
|
35
|
+
removeLabel: string;
|
|
36
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
37
|
+
declare const _default: typeof __VLS_export;
|
|
38
|
+
export default _default;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { useMLMediaAssets } from "../../composables/useMLMediaAssets";
|
|
4
|
+
import { assetName } from "../../utils/assetFormat";
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
assetId: { type: String, required: true },
|
|
7
|
+
siblings: { type: Array, required: false, default: () => [] },
|
|
8
|
+
alt: { type: Object, required: false, default: () => ({}) },
|
|
9
|
+
readonly: { type: Boolean, required: false, default: false },
|
|
10
|
+
removeLabel: { type: String, required: false, default: "" },
|
|
11
|
+
usages: { type: Object, required: false, default: void 0 }
|
|
12
|
+
});
|
|
13
|
+
const emit = defineEmits(["save", "remove", "cancel"]);
|
|
14
|
+
const list = computed(() => {
|
|
15
|
+
const s = props.siblings.filter(Boolean);
|
|
16
|
+
return s.includes(props.assetId) ? s : [props.assetId, ...s];
|
|
17
|
+
});
|
|
18
|
+
const index = ref(Math.max(0, list.value.indexOf(props.assetId)));
|
|
19
|
+
const currentId = computed(() => list.value[index.value] ?? props.assetId);
|
|
20
|
+
const library = useMLMediaAssets(list);
|
|
21
|
+
const asset = computed(() => library.get(currentId.value));
|
|
22
|
+
const lookup = computed(() => library.lookup(currentId.value));
|
|
23
|
+
const name = computed(() => asset.value ? assetName(asset.value) : "");
|
|
24
|
+
const kind = computed(() => (asset.value?.kind ?? "").toLowerCase());
|
|
25
|
+
const takesAlt = computed(() => kind.value === "image" || kind.value === "video");
|
|
26
|
+
const alts = ref({ ...props.alt });
|
|
27
|
+
const dirty = computed(() => list.value.some((id) => (alts.value[id] ?? "") !== (props.alt[id] ?? "")));
|
|
28
|
+
const altModel = computed({
|
|
29
|
+
get: () => alts.value[currentId.value] ?? "",
|
|
30
|
+
set: (v) => {
|
|
31
|
+
alts.value = { ...alts.value, [currentId.value]: v };
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const canPrev = computed(() => index.value > 0);
|
|
35
|
+
const canNext = computed(() => index.value < list.value.length - 1);
|
|
36
|
+
const go = (step) => {
|
|
37
|
+
index.value = Math.min(list.value.length - 1, Math.max(0, index.value + step));
|
|
38
|
+
};
|
|
39
|
+
function onKey(e) {
|
|
40
|
+
const t = e.target;
|
|
41
|
+
if (t && (t.tagName === "TEXTAREA" || t.tagName === "INPUT" || t.isContentEditable)) return;
|
|
42
|
+
if (e.key === "ArrowLeft" && canPrev.value) go(-1);
|
|
43
|
+
else if (e.key === "ArrowRight" && canNext.value) go(1);
|
|
44
|
+
}
|
|
45
|
+
const confirmRemove = ref(false);
|
|
46
|
+
watch(currentId, () => {
|
|
47
|
+
confirmRemove.value = false;
|
|
48
|
+
});
|
|
49
|
+
const usagesHere = computed(() => props.usages?.[currentId.value]);
|
|
50
|
+
const originalUrl = computed(() => asset.value?.previewUrl ?? null);
|
|
51
|
+
defineExpose({ currentId });
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<div
|
|
56
|
+
class="flex min-h-0 flex-col"
|
|
57
|
+
@keydown="onKey"
|
|
58
|
+
>
|
|
59
|
+
<div class="grid min-h-0 md:grid-cols-[minmax(0,1fr)_18rem]">
|
|
60
|
+
<!-- Stage -->
|
|
61
|
+
<div class="relative grid min-h-72 place-items-center bg-neutral-950 px-12 py-10 md:min-h-[26rem]">
|
|
62
|
+
<USkeleton
|
|
63
|
+
v-if="lookup === 'loading'"
|
|
64
|
+
class="aspect-[4/3] w-full max-w-lg rounded-md bg-white/10"
|
|
65
|
+
/>
|
|
66
|
+
<div
|
|
67
|
+
v-else-if="!asset"
|
|
68
|
+
class="flex flex-col items-center gap-2 text-center text-sm text-neutral-300"
|
|
69
|
+
>
|
|
70
|
+
<UIcon
|
|
71
|
+
name="i-lucide-file-x"
|
|
72
|
+
class="size-8 text-neutral-400"
|
|
73
|
+
/>
|
|
74
|
+
{{ lookup === "forbidden" ? "You don't have access to this file." : "This file could not be found." }}
|
|
75
|
+
</div>
|
|
76
|
+
<div
|
|
77
|
+
v-else
|
|
78
|
+
:key="currentId"
|
|
79
|
+
class="h-[min(52vh,26rem)] w-full max-w-2xl"
|
|
80
|
+
>
|
|
81
|
+
<MLAssetManagerAssetPreview :asset="asset" />
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<UButton
|
|
85
|
+
v-if="list.length > 1"
|
|
86
|
+
icon="i-lucide-chevron-left"
|
|
87
|
+
color="neutral"
|
|
88
|
+
variant="soft"
|
|
89
|
+
class="absolute left-2 top-1/2 -translate-y-1/2 rounded-full"
|
|
90
|
+
aria-label="Previous file"
|
|
91
|
+
:disabled="!canPrev"
|
|
92
|
+
@click="go(-1)"
|
|
93
|
+
/>
|
|
94
|
+
<UButton
|
|
95
|
+
v-if="list.length > 1"
|
|
96
|
+
icon="i-lucide-chevron-right"
|
|
97
|
+
color="neutral"
|
|
98
|
+
variant="soft"
|
|
99
|
+
class="absolute right-2 top-1/2 -translate-y-1/2 rounded-full"
|
|
100
|
+
aria-label="Next file"
|
|
101
|
+
:disabled="!canNext"
|
|
102
|
+
@click="go(1)"
|
|
103
|
+
/>
|
|
104
|
+
<span
|
|
105
|
+
v-if="list.length > 1"
|
|
106
|
+
class="absolute bottom-2.5 left-1/2 -translate-x-1/2 text-xs text-neutral-300 tabular-nums"
|
|
107
|
+
aria-live="polite"
|
|
108
|
+
>{{ index + 1 }} of {{ list.length }}</span>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<!-- Side panel -->
|
|
112
|
+
<div class="flex min-w-0 flex-col gap-5 border-t border-default p-4 md:border-l md:border-t-0">
|
|
113
|
+
<div class="flex min-w-0 items-start gap-2">
|
|
114
|
+
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
115
|
+
<span class="truncate text-sm font-semibold">{{ name || "\u2014" }}</span>
|
|
116
|
+
<div
|
|
117
|
+
v-if="asset"
|
|
118
|
+
class="flex flex-wrap gap-1"
|
|
119
|
+
>
|
|
120
|
+
<MLMediaKindBadge :kind="asset.kind" />
|
|
121
|
+
<MLMediaStatusBadge
|
|
122
|
+
v-if="asset.status !== 'ready'"
|
|
123
|
+
:status="asset.status"
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<UButton
|
|
128
|
+
v-if="originalUrl"
|
|
129
|
+
:to="originalUrl"
|
|
130
|
+
target="_blank"
|
|
131
|
+
rel="noopener"
|
|
132
|
+
icon="i-lucide-external-link"
|
|
133
|
+
color="neutral"
|
|
134
|
+
variant="ghost"
|
|
135
|
+
size="sm"
|
|
136
|
+
aria-label="Open the original in a new tab"
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<UFormField
|
|
141
|
+
v-if="takesAlt && !readonly"
|
|
142
|
+
label="Alt text"
|
|
143
|
+
:help="kind === 'image' ? 'Describes the image for screen readers and search.' : 'Describes the video for screen readers.'"
|
|
144
|
+
>
|
|
145
|
+
<UTextarea
|
|
146
|
+
v-model="altModel"
|
|
147
|
+
:rows="3"
|
|
148
|
+
autoresize
|
|
149
|
+
class="w-full"
|
|
150
|
+
:placeholder="kind === 'image' ? 'e.g. Oak lounge chair, front view on a light background' : ''"
|
|
151
|
+
/>
|
|
152
|
+
</UFormField>
|
|
153
|
+
<p
|
|
154
|
+
v-else-if="takesAlt && altModel"
|
|
155
|
+
class="text-sm"
|
|
156
|
+
>
|
|
157
|
+
<span class="block text-[11px] font-semibold uppercase tracking-wider text-dimmed">Alt text</span>
|
|
158
|
+
{{ altModel }}
|
|
159
|
+
</p>
|
|
160
|
+
|
|
161
|
+
<MLMediaFileFacts :asset="asset" />
|
|
162
|
+
|
|
163
|
+
<MLMediaUsageList
|
|
164
|
+
v-if="usagesHere"
|
|
165
|
+
:usages="usagesHere"
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="flex flex-wrap items-center gap-2 border-t border-default px-4 py-3">
|
|
171
|
+
<template v-if="removeLabel && !readonly">
|
|
172
|
+
<UButton
|
|
173
|
+
v-if="!confirmRemove"
|
|
174
|
+
color="error"
|
|
175
|
+
variant="ghost"
|
|
176
|
+
icon="i-lucide-x"
|
|
177
|
+
:label="removeLabel"
|
|
178
|
+
@click="confirmRemove = true"
|
|
179
|
+
/>
|
|
180
|
+
<template v-else>
|
|
181
|
+
<span class="text-sm text-muted">The file stays in the library.</span>
|
|
182
|
+
<UButton
|
|
183
|
+
color="error"
|
|
184
|
+
variant="soft"
|
|
185
|
+
:label="removeLabel"
|
|
186
|
+
@click="emit('remove', currentId)"
|
|
187
|
+
/>
|
|
188
|
+
<UButton
|
|
189
|
+
color="neutral"
|
|
190
|
+
variant="ghost"
|
|
191
|
+
label="Keep"
|
|
192
|
+
@click="confirmRemove = false"
|
|
193
|
+
/>
|
|
194
|
+
</template>
|
|
195
|
+
</template>
|
|
196
|
+
<span class="me-auto" />
|
|
197
|
+
<template v-if="readonly">
|
|
198
|
+
<UButton
|
|
199
|
+
color="neutral"
|
|
200
|
+
variant="outline"
|
|
201
|
+
label="Close"
|
|
202
|
+
@click="emit('cancel')"
|
|
203
|
+
/>
|
|
204
|
+
</template>
|
|
205
|
+
<template v-else>
|
|
206
|
+
<UButton
|
|
207
|
+
color="neutral"
|
|
208
|
+
variant="outline"
|
|
209
|
+
label="Cancel"
|
|
210
|
+
@click="emit('cancel')"
|
|
211
|
+
/>
|
|
212
|
+
<UButton
|
|
213
|
+
label="Save"
|
|
214
|
+
:disabled="!dirty"
|
|
215
|
+
@click="emit('save', { alt: { ...alts } })"
|
|
216
|
+
/>
|
|
217
|
+
</template>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</template>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MediaUsage } from './UsageList.vue.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** The file to show first. */
|
|
4
|
+
assetId: string;
|
|
5
|
+
/** The record's other files, in order. Enables previous / next. */
|
|
6
|
+
siblings?: readonly string[];
|
|
7
|
+
/** Current alt text per asset ID. Alt text lives on the host record, not the file. */
|
|
8
|
+
alt?: Readonly<Record<string, string>>;
|
|
9
|
+
/** Preview only: no alt editing, no Save. Used from the picker. */
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
/** Label for the remove button, e.g. "Remove from product". Empty hides it. */
|
|
12
|
+
removeLabel?: string;
|
|
13
|
+
/** Where each file is used, per asset ID. Omit to hide the section. */
|
|
14
|
+
usages?: Readonly<Record<string, MediaUsage[]>>;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
17
|
+
currentId: import("vue").ComputedRef<string>;
|
|
18
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
|
+
remove: (assetId: string) => any;
|
|
20
|
+
cancel: () => any;
|
|
21
|
+
save: (result: {
|
|
22
|
+
alt: Record<string, string>;
|
|
23
|
+
}) => any;
|
|
24
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
25
|
+
onRemove?: ((assetId: string) => any) | undefined;
|
|
26
|
+
onCancel?: (() => any) | undefined;
|
|
27
|
+
onSave?: ((result: {
|
|
28
|
+
alt: Record<string, string>;
|
|
29
|
+
}) => any) | undefined;
|
|
30
|
+
}>, {
|
|
31
|
+
usages: Readonly<Record<string, MediaUsage[]>>;
|
|
32
|
+
siblings: readonly string[];
|
|
33
|
+
alt: Readonly<Record<string, string>>;
|
|
34
|
+
readonly: boolean;
|
|
35
|
+
removeLabel: string;
|
|
36
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
37
|
+
declare const _default: typeof __VLS_export;
|
|
38
|
+
export default _default;
|