@revenexx/studio-shared 0.2.0 → 0.3.1
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.d.mts +7 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +37 -1
- package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.vue +93 -36
- package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -0
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +53 -4
- package/dist/runtime/components/DataTable/DataTable.vue +274 -118
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +53 -4
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +14 -3
- package/dist/runtime/components/DataTable/FilterMenu.vue +35 -29
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +14 -3
- package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -0
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue +189 -0
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -0
- package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +13 -0
- package/dist/runtime/components/DataTable/SortMenu.vue +47 -0
- package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +13 -0
- package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +20 -0
- package/dist/runtime/components/DataTable/ViewModeSwitch.vue +26 -0
- package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +20 -0
- package/dist/runtime/components/DataTable/types.d.ts +49 -2
- package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +17 -0
- package/dist/runtime/components/Media/MediaFolderTree.vue +65 -0
- package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +17 -0
- package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Media/MediaPickerDialog.vue +163 -0
- package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Media/MediaUploader.d.vue.ts +29 -0
- package/dist/runtime/components/Media/MediaUploader.vue +161 -0
- package/dist/runtime/components/Media/MediaUploader.vue.d.ts +29 -0
- package/dist/runtime/components/Shared/BuildLogsFlyout.vue +14 -13
- package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +55 -0
- package/dist/runtime/components/Shared/FloatingPanel.vue +118 -0
- package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +55 -0
- package/dist/runtime/components/Shared/GitDeploymentSettings.vue +11 -10
- package/dist/runtime/components/Shared/ListSurface.d.vue.ts +38 -0
- package/dist/runtime/components/Shared/ListSurface.vue +27 -0
- package/dist/runtime/components/Shared/ListSurface.vue.d.ts +38 -0
- package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +52 -0
- package/dist/runtime/components/Shared/RecordLayout.vue +63 -0
- package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +52 -0
- package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +26 -0
- package/dist/runtime/components/Shared/RecordRailCard.vue +20 -0
- package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +26 -0
- package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +5 -0
- package/dist/runtime/components/Shared/SidebarNav.vue +6 -26
- package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +5 -0
- package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +15 -0
- package/dist/runtime/components/Shared/SidebarNavLink.vue +49 -0
- package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +15 -0
- package/dist/runtime/components/Shared/Wizard.vue +6 -3
- package/dist/runtime/composables/useFloatingPanel.d.ts +69 -0
- package/dist/runtime/composables/useFloatingPanel.js +112 -0
- package/dist/runtime/composables/useListRail.d.ts +9 -0
- package/dist/runtime/composables/useListRail.js +19 -0
- package/dist/runtime/composables/useMediaLibrary.d.ts +41 -0
- package/dist/runtime/composables/useMediaLibrary.js +36 -0
- package/dist/runtime/composables/usePlatformAuth.d.ts +18 -0
- package/dist/runtime/composables/usePlatformTalkback.d.ts +29 -0
- package/dist/runtime/composables/usePlatformTalkback.js +54 -0
- package/dist/runtime/composables/useT.d.ts +11 -0
- package/dist/runtime/composables/useT.js +8 -0
- package/dist/runtime/composables/useTalkbackChannels.d.ts +93 -0
- package/dist/runtime/composables/useTalkbackChannels.js +56 -0
- package/dist/runtime/lib/columnOrder.d.ts +45 -0
- package/dist/runtime/lib/columnOrder.js +28 -0
- package/dist/runtime/lib/columnVisibility.d.ts +41 -0
- package/dist/runtime/lib/columnVisibility.js +12 -0
- package/dist/runtime/lib/dataTableFilters.d.ts +83 -0
- package/dist/runtime/lib/dataTableFilters.js +23 -0
- package/dist/runtime/lib/formatBridge.d.ts +50 -0
- package/dist/runtime/lib/formatBridge.js +54 -0
- package/dist/runtime/lib/i18nBridge.d.ts +36 -0
- package/dist/runtime/lib/i18nBridge.js +17 -0
- package/dist/runtime/lib/isoLists.d.ts +61 -0
- package/dist/runtime/lib/isoLists.js +62 -0
- package/dist/runtime/lib/sidebarNav.d.ts +34 -0
- package/dist/runtime/lib/sidebarNav.js +22 -0
- package/dist/runtime/lib/talkback.d.ts +59 -0
- package/dist/runtime/lib/talkback.js +49 -0
- package/dist/runtime/types/nav.d.ts +15 -1
- package/dist/runtime/types/storage.d.ts +67 -0
- package/dist/runtime/types/storage.js +0 -0
- package/dist/types.d.mts +14 -0
- package/package.json +19 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@revenexx/studio";
|
|
3
|
+
import { CloudUpload, FileText } from "#solar-icons";
|
|
4
|
+
import MediaFolderTree from "./MediaFolderTree.vue";
|
|
5
|
+
import MediaUploader from "./MediaUploader.vue";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
open: { type: Boolean, required: true },
|
|
8
|
+
multiple: { type: Boolean, required: false, default: false },
|
|
9
|
+
kinds: { type: Array, required: false, default: void 0 }
|
|
10
|
+
});
|
|
11
|
+
const emit = defineEmits(["update:open", "select"]);
|
|
12
|
+
const {
|
|
13
|
+
assets,
|
|
14
|
+
folderTree,
|
|
15
|
+
currentFolderId,
|
|
16
|
+
currentView,
|
|
17
|
+
currentFolder,
|
|
18
|
+
loadingAssets,
|
|
19
|
+
hasMore,
|
|
20
|
+
init,
|
|
21
|
+
openFolder,
|
|
22
|
+
showAllMedia,
|
|
23
|
+
loadMore,
|
|
24
|
+
thumbnailUrl,
|
|
25
|
+
isProcessing
|
|
26
|
+
} = useMediaLibrary();
|
|
27
|
+
const t = useT();
|
|
28
|
+
const selected = ref([]);
|
|
29
|
+
const uploadOpen = ref(false);
|
|
30
|
+
const failedThumbs = ref(/* @__PURE__ */ new Set());
|
|
31
|
+
const kindIcon = {};
|
|
32
|
+
const visibleAssets = computed(
|
|
33
|
+
() => props.kinds?.length ? assets.value.filter((a) => props.kinds.includes(a.kind)) : assets.value
|
|
34
|
+
);
|
|
35
|
+
function isSelected(asset) {
|
|
36
|
+
return selected.value.some((a) => a.id === asset.id);
|
|
37
|
+
}
|
|
38
|
+
function toggle(asset) {
|
|
39
|
+
if (isProcessing(asset)) return;
|
|
40
|
+
if (props.multiple) {
|
|
41
|
+
if (isSelected(asset)) selected.value = selected.value.filter((a) => a.id !== asset.id);
|
|
42
|
+
else selected.value = [...selected.value, asset];
|
|
43
|
+
} else {
|
|
44
|
+
selected.value = isSelected(asset) ? [] : [asset];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function markFailed(url) {
|
|
48
|
+
if (url) failedThumbs.value = new Set(failedThumbs.value).add(url);
|
|
49
|
+
}
|
|
50
|
+
function confirm() {
|
|
51
|
+
if (!selected.value.length) return;
|
|
52
|
+
emit("select", selected.value);
|
|
53
|
+
emit("update:open", false);
|
|
54
|
+
}
|
|
55
|
+
function onUploaded() {
|
|
56
|
+
uploadOpen.value = false;
|
|
57
|
+
}
|
|
58
|
+
watch(() => props.open, (open) => {
|
|
59
|
+
if (open) {
|
|
60
|
+
selected.value = [];
|
|
61
|
+
void init();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
68
|
+
<DialogContent class="max-w-4xl">
|
|
69
|
+
<DialogHeader>
|
|
70
|
+
<DialogTitle>{{ multiple ? t("shared.media.picker.title.multiple") : t("shared.media.picker.title.single") }}</DialogTitle>
|
|
71
|
+
</DialogHeader>
|
|
72
|
+
|
|
73
|
+
<div class="flex gap-4 h-[26rem]">
|
|
74
|
+
<!-- Folder sidebar -->
|
|
75
|
+
<div class="w-56 shrink-0 overflow-y-auto border-r border-black/[0.06] pr-2">
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
class="w-full text-left rounded-md px-2 py-1 text-sm transition-colors"
|
|
79
|
+
:class="currentView === 'all' ? 'bg-primary/10 text-[#0B0522] font-medium' : 'text-foreground/80 hover:bg-primary/5'"
|
|
80
|
+
@click="showAllMedia"
|
|
81
|
+
>
|
|
82
|
+
{{ t("shared.media.picker.action.allmedia") }}
|
|
83
|
+
</button>
|
|
84
|
+
<MediaFolderTree
|
|
85
|
+
:nodes="folderTree"
|
|
86
|
+
:current-id="currentView === 'folder' ? currentFolderId : null"
|
|
87
|
+
@select="(id) => id ? openFolder(id) : showAllMedia()"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Asset grid -->
|
|
92
|
+
<div class="flex-1 overflow-y-auto">
|
|
93
|
+
<div class="flex items-center justify-between mb-2">
|
|
94
|
+
<p class="text-sm text-muted-foreground">
|
|
95
|
+
{{ t("shared.media.picker.count", visibleAssets.length) }}
|
|
96
|
+
</p>
|
|
97
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" @click="uploadOpen = true">
|
|
98
|
+
<CloudUpload :size="14" /> {{ t("shared.media.picker.action.upload") }}
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div v-if="loadingAssets && !visibleAssets.length" class="py-12 text-center text-sm text-muted-foreground">
|
|
103
|
+
{{ t("shared.media.picker.loading") }}
|
|
104
|
+
</div>
|
|
105
|
+
<div v-else-if="!visibleAssets.length" class="py-12 text-center text-sm text-muted-foreground">
|
|
106
|
+
{{ t("shared.media.picker.empty") }}
|
|
107
|
+
</div>
|
|
108
|
+
<div v-else class="grid grid-cols-3 gap-3 sm:grid-cols-4">
|
|
109
|
+
<button
|
|
110
|
+
v-for="asset in visibleAssets"
|
|
111
|
+
:key="asset.id"
|
|
112
|
+
type="button"
|
|
113
|
+
class="group relative aspect-square rounded-md border overflow-hidden transition-all"
|
|
114
|
+
:class="isSelected(asset) ? 'border-primary ring-2 ring-primary' : 'border-black/[0.06] hover:border-primary/40'"
|
|
115
|
+
@click="toggle(asset)"
|
|
116
|
+
>
|
|
117
|
+
<img
|
|
118
|
+
v-if="thumbnailUrl(asset) && !failedThumbs.has(thumbnailUrl(asset))"
|
|
119
|
+
:src="thumbnailUrl(asset)"
|
|
120
|
+
:alt="asset.alt_text ?? asset.display_name ?? asset.original_name"
|
|
121
|
+
class="h-full w-full object-cover"
|
|
122
|
+
:class="{ 'opacity-50': isProcessing(asset) }"
|
|
123
|
+
@error="markFailed(thumbnailUrl(asset))"
|
|
124
|
+
>
|
|
125
|
+
<span v-else class="flex h-full w-full items-center justify-center">
|
|
126
|
+
<component :is="kindIcon[asset.kind] ?? FileText" :size="28" class="text-foreground/30" />
|
|
127
|
+
</span>
|
|
128
|
+
<span class="absolute inset-x-0 bottom-0 truncate bg-black/50 px-1.5 py-0.5 text-[11px] text-white">
|
|
129
|
+
{{ asset.display_name ?? asset.original_name }}
|
|
130
|
+
</span>
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div v-if="hasMore" class="mt-3 flex justify-center">
|
|
135
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="loadingAssets" @click="loadMore">
|
|
136
|
+
{{ t("shared.media.picker.action.loadmore") }}
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<DialogFooter>
|
|
143
|
+
<span class="mr-auto text-sm text-muted-foreground">
|
|
144
|
+
{{ t("shared.media.picker.selectedcount", { count: selected.length }) }}
|
|
145
|
+
</span>
|
|
146
|
+
<Button variant="outline" size="sm" @click="emit('update:open', false)">
|
|
147
|
+
{{ t("shared.media.picker.action.cancel") }}
|
|
148
|
+
</Button>
|
|
149
|
+
<Button size="sm" :disabled="!selected.length" @click="confirm">
|
|
150
|
+
{{ t("shared.media.picker.action.confirm") }}
|
|
151
|
+
</Button>
|
|
152
|
+
</DialogFooter>
|
|
153
|
+
</DialogContent>
|
|
154
|
+
</Dialog>
|
|
155
|
+
|
|
156
|
+
<MediaUploader
|
|
157
|
+
:open="uploadOpen"
|
|
158
|
+
:folder-id="currentFolderId"
|
|
159
|
+
:folder-name="currentFolder?.name ?? null"
|
|
160
|
+
@update:open="(v) => uploadOpen = v"
|
|
161
|
+
@done="onUploaded"
|
|
162
|
+
/>
|
|
163
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StorageAsset } from '../../types/storage.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
multiple?: boolean;
|
|
5
|
+
/** Restrict the grid to these asset kinds (e.g. ['image']). */
|
|
6
|
+
kinds?: string[];
|
|
7
|
+
};
|
|
8
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
select: (assets: StorageAsset[]) => any;
|
|
10
|
+
"update:open": (value: boolean) => any;
|
|
11
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
12
|
+
onSelect?: ((assets: StorageAsset[]) => any) | undefined;
|
|
13
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
14
|
+
}>, {
|
|
15
|
+
multiple: boolean;
|
|
16
|
+
kinds: string[];
|
|
17
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload dialog for the media browser. Drag-and-drop or pick multiple files;
|
|
3
|
+
* they're uploaded into the active folder (single → /assets, many → batched
|
|
4
|
+
* /assets/bulk) with per-file progress. Emits `done` once a batch finishes so
|
|
5
|
+
* the page can flip back to the grid.
|
|
6
|
+
*
|
|
7
|
+
* The storage client arrives through the `useMediaLibrary` provider seam, so
|
|
8
|
+
* this works in any host that wires one (the cockpit does; a standalone module
|
|
9
|
+
* host gets the inert default).
|
|
10
|
+
*/
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
open: boolean;
|
|
13
|
+
folderId: string | null;
|
|
14
|
+
folderName: string | null;
|
|
15
|
+
};
|
|
16
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
done: (summary: {
|
|
18
|
+
created: number;
|
|
19
|
+
failed: number;
|
|
20
|
+
}) => any;
|
|
21
|
+
"update:open": (value: boolean) => any;
|
|
22
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
23
|
+
onDone?: ((summary: {
|
|
24
|
+
created: number;
|
|
25
|
+
failed: number;
|
|
26
|
+
}) => any) | undefined;
|
|
27
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
28
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
|
+
export default _default;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { CloudUpload, CheckCircle, CloseCircle, RefreshCircle as Loader2 } from "#solar-icons";
|
|
3
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Progress } from "@revenexx/studio";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
open: { type: Boolean, required: true },
|
|
6
|
+
folderId: { type: [String, null], required: true },
|
|
7
|
+
folderName: { type: [String, null], required: true }
|
|
8
|
+
});
|
|
9
|
+
const emit = defineEmits(["update:open", "done"]);
|
|
10
|
+
const { uploadMany } = useMediaLibrary();
|
|
11
|
+
const t = useT();
|
|
12
|
+
const files = ref([]);
|
|
13
|
+
const progress = ref([]);
|
|
14
|
+
const uploading = ref(false);
|
|
15
|
+
const dragOver = ref(false);
|
|
16
|
+
const fileInput = ref(null);
|
|
17
|
+
function formatBytes(bytes, fractionDigits = 1) {
|
|
18
|
+
if (bytes === null || bytes === void 0 || bytes < 0) return "\u2014";
|
|
19
|
+
if (bytes < 1024) return `${bytes} ${t("shared.media.uploader.unit.bytes")}`;
|
|
20
|
+
const units = [
|
|
21
|
+
t("shared.media.uploader.unit.kb"),
|
|
22
|
+
t("shared.media.uploader.unit.mb"),
|
|
23
|
+
t("shared.media.uploader.unit.gb"),
|
|
24
|
+
t("shared.media.uploader.unit.tb")
|
|
25
|
+
];
|
|
26
|
+
let value = bytes / 1024;
|
|
27
|
+
let unit = 0;
|
|
28
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
29
|
+
value /= 1024;
|
|
30
|
+
unit++;
|
|
31
|
+
}
|
|
32
|
+
return `${value.toFixed(fractionDigits)} ${units[unit]}`;
|
|
33
|
+
}
|
|
34
|
+
function reset() {
|
|
35
|
+
files.value = [];
|
|
36
|
+
progress.value = [];
|
|
37
|
+
uploading.value = false;
|
|
38
|
+
dragOver.value = false;
|
|
39
|
+
}
|
|
40
|
+
watch(() => props.open, (isOpen) => {
|
|
41
|
+
if (!isOpen) reset();
|
|
42
|
+
});
|
|
43
|
+
function addFiles(list) {
|
|
44
|
+
if (!list) return;
|
|
45
|
+
files.value = [...files.value, ...Array.from(list)];
|
|
46
|
+
}
|
|
47
|
+
function onDrop(e) {
|
|
48
|
+
dragOver.value = false;
|
|
49
|
+
addFiles(e.dataTransfer?.files ?? null);
|
|
50
|
+
}
|
|
51
|
+
function onPick(e) {
|
|
52
|
+
addFiles(e.target.files);
|
|
53
|
+
if (fileInput.value) fileInput.value.value = "";
|
|
54
|
+
}
|
|
55
|
+
function removeFile(index) {
|
|
56
|
+
files.value = files.value.filter((_, i) => i !== index);
|
|
57
|
+
}
|
|
58
|
+
const totalBytes = computed(() => files.value.reduce((sum, f) => sum + f.size, 0));
|
|
59
|
+
async function start() {
|
|
60
|
+
if (!files.value.length || uploading.value) return;
|
|
61
|
+
uploading.value = true;
|
|
62
|
+
progress.value = files.value.map((f) => ({ name: f.name, pct: 0, status: "pending" }));
|
|
63
|
+
const summary = await uploadMany(files.value, props.folderId, (index, p) => {
|
|
64
|
+
progress.value[index] = p;
|
|
65
|
+
});
|
|
66
|
+
uploading.value = false;
|
|
67
|
+
emit("done", summary);
|
|
68
|
+
if (summary.failed === 0) {
|
|
69
|
+
setTimeout(() => emit("update:open", false), 600);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function statusIcon(s) {
|
|
73
|
+
return s === "done" ? CheckCircle : s === "failed" ? CloseCircle : Loader2;
|
|
74
|
+
}
|
|
75
|
+
const descriptionParts = computed(() => {
|
|
76
|
+
const sentinel = "\0";
|
|
77
|
+
const [before = "", after = ""] = t("shared.media.uploader.description", { folder: sentinel }).split(sentinel);
|
|
78
|
+
return { before, after };
|
|
79
|
+
});
|
|
80
|
+
const folderLabel = computed(() => props.folderName ?? t("shared.media.uploader.folder.default"));
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
85
|
+
<DialogContent class="max-w-lg">
|
|
86
|
+
<DialogHeader>
|
|
87
|
+
<DialogTitle>{{ t("shared.media.uploader.title") }}</DialogTitle>
|
|
88
|
+
<DialogDescription>
|
|
89
|
+
{{ descriptionParts.before }}<span class="font-medium text-foreground">{{ folderLabel }}</span>{{ descriptionParts.after }}
|
|
90
|
+
</DialogDescription>
|
|
91
|
+
</DialogHeader>
|
|
92
|
+
|
|
93
|
+
<!-- Drop zone -->
|
|
94
|
+
<div
|
|
95
|
+
class="flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors"
|
|
96
|
+
:class="dragOver ? 'border-primary bg-primary/5' : 'border-input bg-muted/30'"
|
|
97
|
+
@dragover.prevent="dragOver = true"
|
|
98
|
+
@dragleave.prevent="dragOver = false"
|
|
99
|
+
@drop.prevent="onDrop"
|
|
100
|
+
>
|
|
101
|
+
<CloudUpload :size="28" class="text-foreground/40" />
|
|
102
|
+
<p class="text-sm text-muted-foreground">
|
|
103
|
+
{{ t("shared.media.uploader.dropzone.text") }}
|
|
104
|
+
<button type="button" class="font-medium text-primary hover:underline" @click="fileInput?.click()">{{ t("shared.media.uploader.action.browse") }}</button>
|
|
105
|
+
</p>
|
|
106
|
+
<p class="text-xs text-muted-foreground/70">{{ t("shared.media.uploader.dropzone.hint") }}</p>
|
|
107
|
+
<input ref="fileInput" type="file" multiple class="hidden" @change="onPick">
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Selected / progress list -->
|
|
111
|
+
<div v-if="files.length" class="max-h-56 space-y-1.5 overflow-y-auto">
|
|
112
|
+
<div
|
|
113
|
+
v-for="(file, i) in files"
|
|
114
|
+
:key="`${file.name}-${i}`"
|
|
115
|
+
class="flex items-center gap-2 rounded-md border border-black/[0.06] bg-white px-2.5 py-1.5 text-sm"
|
|
116
|
+
>
|
|
117
|
+
<component
|
|
118
|
+
:is="statusIcon(progress[i]?.status ?? 'pending')"
|
|
119
|
+
:size="14"
|
|
120
|
+
class="shrink-0"
|
|
121
|
+
:class="{
|
|
122
|
+
'text-emerald-600': progress[i]?.status === 'done',
|
|
123
|
+
'text-red-600': progress[i]?.status === 'failed',
|
|
124
|
+
'animate-spin text-amber-500': uploading && (progress[i]?.status === 'uploading' || progress[i]?.status === 'pending'),
|
|
125
|
+
'text-foreground/30': !uploading
|
|
126
|
+
}"
|
|
127
|
+
/>
|
|
128
|
+
<div class="min-w-0 flex-1">
|
|
129
|
+
<div class="flex items-center justify-between gap-2">
|
|
130
|
+
<span class="truncate font-medium text-[#0B0522]">{{ file.name }}</span>
|
|
131
|
+
<span class="shrink-0 tabular-nums text-xs text-muted-foreground">{{ formatBytes(file.size) }}</span>
|
|
132
|
+
</div>
|
|
133
|
+
<Progress v-if="uploading" :model-value="progress[i]?.pct ?? 0" class="mt-1 h-1" />
|
|
134
|
+
<p v-if="progress[i]?.status === 'failed'" class="mt-0.5 text-xs text-red-600">{{ progress[i]?.error }}</p>
|
|
135
|
+
</div>
|
|
136
|
+
<button
|
|
137
|
+
v-if="!uploading"
|
|
138
|
+
type="button"
|
|
139
|
+
class="shrink-0 rounded p-0.5 text-muted-foreground hover:text-red-600"
|
|
140
|
+
@click="removeFile(i)"
|
|
141
|
+
>
|
|
142
|
+
<CloseCircle :size="14" />
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<DialogFooter class="items-center">
|
|
148
|
+
<span v-if="files.length" class="mr-auto text-xs text-muted-foreground">
|
|
149
|
+
{{ t("shared.media.uploader.filecount", files.length) }} · {{ formatBytes(totalBytes) }}
|
|
150
|
+
</span>
|
|
151
|
+
<Button variant="outline" :disabled="uploading" @click="emit('update:open', false)">
|
|
152
|
+
{{ t("shared.media.uploader.action.cancel") }}
|
|
153
|
+
</Button>
|
|
154
|
+
<Button :disabled="!files.length || uploading" @click="start">
|
|
155
|
+
<component :is="uploading ? Loader2 : CloudUpload" :size="14" :class="{ 'animate-spin': uploading }" />
|
|
156
|
+
{{ uploading ? t("shared.media.uploader.action.uploading") : t("shared.media.uploader.action.upload", { count: files.length || "" }).trim() }}
|
|
157
|
+
</Button>
|
|
158
|
+
</DialogFooter>
|
|
159
|
+
</DialogContent>
|
|
160
|
+
</Dialog>
|
|
161
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload dialog for the media browser. Drag-and-drop or pick multiple files;
|
|
3
|
+
* they're uploaded into the active folder (single → /assets, many → batched
|
|
4
|
+
* /assets/bulk) with per-file progress. Emits `done` once a batch finishes so
|
|
5
|
+
* the page can flip back to the grid.
|
|
6
|
+
*
|
|
7
|
+
* The storage client arrives through the `useMediaLibrary` provider seam, so
|
|
8
|
+
* this works in any host that wires one (the cockpit does; a standalone module
|
|
9
|
+
* host gets the inert default).
|
|
10
|
+
*/
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
open: boolean;
|
|
13
|
+
folderId: string | null;
|
|
14
|
+
folderName: string | null;
|
|
15
|
+
};
|
|
16
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
done: (summary: {
|
|
18
|
+
created: number;
|
|
19
|
+
failed: number;
|
|
20
|
+
}) => any;
|
|
21
|
+
"update:open": (value: boolean) => any;
|
|
22
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
23
|
+
onDone?: ((summary: {
|
|
24
|
+
created: number;
|
|
25
|
+
failed: number;
|
|
26
|
+
}) => any) | undefined;
|
|
27
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
28
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
|
+
export default _default;
|
|
@@ -19,13 +19,14 @@ defineProps({
|
|
|
19
19
|
error: { type: [String, null], required: false }
|
|
20
20
|
});
|
|
21
21
|
const emit = defineEmits(["update:open", "refresh"]);
|
|
22
|
+
const t = useT();
|
|
22
23
|
async function copyId(id) {
|
|
23
24
|
if (!id) return;
|
|
24
25
|
try {
|
|
25
26
|
await navigator.clipboard.writeText(id);
|
|
26
|
-
toast.success("
|
|
27
|
+
toast.success(t("shared.buildlogs.toast.idcopied"));
|
|
27
28
|
} catch {
|
|
28
|
-
toast.error("
|
|
29
|
+
toast.error(t("shared.buildlogs.toast.copyfailed"));
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
</script>
|
|
@@ -35,11 +36,11 @@ async function copyId(id) {
|
|
|
35
36
|
<SheetContent side="right" class="flex w-full flex-col gap-0 p-0 sm:max-w-[max(50vw,640px)]">
|
|
36
37
|
<SheetHeader class="border-b px-4 py-3">
|
|
37
38
|
<div class="flex items-center gap-2">
|
|
38
|
-
<Badge variant="outline" class="shrink-0">
|
|
39
|
+
<Badge variant="outline" class="shrink-0">{{ t("shared.buildlogs.badge.build") }}</Badge>
|
|
39
40
|
<SheetTitle class="truncate font-mono text-sm">
|
|
40
|
-
{{ deployment?.$id || "
|
|
41
|
+
{{ deployment?.$id || t("shared.buildlogs.title.default") }}
|
|
41
42
|
</SheetTitle>
|
|
42
|
-
<button v-if="deployment?.$id" title="
|
|
43
|
+
<button v-if="deployment?.$id" :title="t('shared.buildlogs.tooltip.copyid')" class="ml-auto" @click="copyId(deployment.$id)">
|
|
43
44
|
<CopyIcon :size="12" class="text-muted-foreground hover:text-foreground" />
|
|
44
45
|
</button>
|
|
45
46
|
</div>
|
|
@@ -58,29 +59,29 @@ async function copyId(id) {
|
|
|
58
59
|
</template>
|
|
59
60
|
<template v-if="userPinned">
|
|
60
61
|
<span class="text-muted-foreground">·</span>
|
|
61
|
-
<span>pinned</span>
|
|
62
|
+
<span>{{ t("shared.buildlogs.badge.pinned") }}</span>
|
|
62
63
|
</template>
|
|
63
64
|
</SheetDescription>
|
|
64
65
|
</SheetHeader>
|
|
65
66
|
|
|
66
67
|
<div class="flex-1 overflow-y-auto">
|
|
67
68
|
<div v-if="loading && !deployment" class="py-8 text-center text-sm text-muted-foreground">
|
|
68
|
-
<Loader2 :size="16" class="mr-2 inline animate-spin" />
|
|
69
|
+
<Loader2 :size="16" class="mr-2 inline animate-spin" /> {{ t("shared.buildlogs.loading") }}
|
|
69
70
|
</div>
|
|
70
71
|
<p v-else-if="error" class="p-4 text-sm text-rose-700">{{ error }}</p>
|
|
71
72
|
|
|
72
73
|
<template v-else-if="deployment">
|
|
73
74
|
<div class="flex items-center justify-between gap-2 px-4 py-2 border-b bg-muted/30">
|
|
74
75
|
<span class="text-xs text-muted-foreground">
|
|
75
|
-
|
|
76
|
+
{{ t("shared.buildlogs.status.label") }} <span class="font-medium text-foreground">{{ deployment.status || "\u2014" }}</span>
|
|
76
77
|
<template v-if="deployment.buildDuration">
|
|
77
|
-
· {{ deployment.buildDuration }}
|
|
78
|
+
· {{ deployment.buildDuration }}{{ t("shared.buildlogs.duration.unit") }}
|
|
78
79
|
</template>
|
|
79
80
|
</span>
|
|
80
81
|
<div class="flex items-center gap-2">
|
|
81
82
|
<Loader2 v-if="isBuilding || loading" :size="12" class="animate-spin text-muted-foreground" />
|
|
82
83
|
<Button variant="ghost" size="sm" class="h-6 text-xs" @click="emit('refresh')">
|
|
83
|
-
|
|
84
|
+
{{ t("shared.buildlogs.action.refresh") }}
|
|
84
85
|
</Button>
|
|
85
86
|
</div>
|
|
86
87
|
</div>
|
|
@@ -92,16 +93,16 @@ async function copyId(id) {
|
|
|
92
93
|
>{{ stripAnsi(deployment.buildLogs) }}</pre>
|
|
93
94
|
<p v-else-if="isBuilding" class="flex items-center gap-2 text-sm text-muted-foreground py-8">
|
|
94
95
|
<Loader2 :size="12" class="animate-spin" />
|
|
95
|
-
|
|
96
|
+
{{ t("shared.buildlogs.waiting") }}
|
|
96
97
|
</p>
|
|
97
98
|
<p v-else class="text-sm text-muted-foreground py-8 text-center">
|
|
98
|
-
|
|
99
|
+
{{ t("shared.buildlogs.nologs") }}
|
|
99
100
|
</p>
|
|
100
101
|
</div>
|
|
101
102
|
</template>
|
|
102
103
|
|
|
103
104
|
<div v-else class="py-12 text-center text-sm text-muted-foreground">
|
|
104
|
-
|
|
105
|
+
{{ t("shared.buildlogs.selectbuild") }}
|
|
105
106
|
</div>
|
|
106
107
|
</div>
|
|
107
108
|
</SheetContent>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
type Mode = 'docked-left' | 'docked-right' | 'floating';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
/** What `useFloatingPanel` returned, passed through whole. */
|
|
4
|
+
panel: {
|
|
5
|
+
mode: {
|
|
6
|
+
value: Mode;
|
|
7
|
+
};
|
|
8
|
+
collapsed: {
|
|
9
|
+
value: boolean;
|
|
10
|
+
};
|
|
11
|
+
floatingStyle: {
|
|
12
|
+
value: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
dragging: {
|
|
15
|
+
value: boolean;
|
|
16
|
+
};
|
|
17
|
+
startDrag: (ev: PointerEvent, el: HTMLElement | null) => void;
|
|
18
|
+
dock: (side: 'left' | 'right') => void;
|
|
19
|
+
floatHere: (el: HTMLElement | null) => void;
|
|
20
|
+
setCollapsed: (v: boolean) => void;
|
|
21
|
+
};
|
|
22
|
+
title: string;
|
|
23
|
+
/** Room the docked panel leaves at the bottom, for a floating control bar. */
|
|
24
|
+
bottomClass?: string;
|
|
25
|
+
/** Tailwind width, matching the `width` given to the composable. */
|
|
26
|
+
widthClass?: string;
|
|
27
|
+
/** Labels, so the package carries no copy of its own. */
|
|
28
|
+
labels: {
|
|
29
|
+
dockLeft: string;
|
|
30
|
+
dockRight: string;
|
|
31
|
+
floatHere: string;
|
|
32
|
+
collapse: string;
|
|
33
|
+
expand: string;
|
|
34
|
+
placement: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
declare var __VLS_5: {}, __VLS_67: {}, __VLS_69: {};
|
|
38
|
+
type __VLS_Slots = {} & {
|
|
39
|
+
'pill-icon'?: (props: typeof __VLS_5) => any;
|
|
40
|
+
} & {
|
|
41
|
+
default?: (props: typeof __VLS_67) => any;
|
|
42
|
+
} & {
|
|
43
|
+
footer?: (props: typeof __VLS_69) => any;
|
|
44
|
+
};
|
|
45
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
46
|
+
bottomClass: string;
|
|
47
|
+
widthClass: string;
|
|
48
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
49
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
50
|
+
export default _default;
|
|
51
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
52
|
+
new (): {
|
|
53
|
+
$slots: S;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { HamburgerMenu, MenuDots, Minimize } from "#solar-icons";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuTrigger
|
|
10
|
+
} from "@revenexx/studio";
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
panel: { type: Object, required: true },
|
|
13
|
+
title: { type: String, required: true },
|
|
14
|
+
bottomClass: { type: String, required: false, default: "bottom-16" },
|
|
15
|
+
widthClass: { type: String, required: false, default: "w-80" },
|
|
16
|
+
labels: { type: Object, required: true }
|
|
17
|
+
});
|
|
18
|
+
const shell = ref(null);
|
|
19
|
+
const shellClass = computed(() => {
|
|
20
|
+
if (props.panel.mode.value === "docked-left") return `left-3 top-3 ${props.bottomClass}`;
|
|
21
|
+
if (props.panel.mode.value === "docked-right") return `right-3 top-3 ${props.bottomClass}`;
|
|
22
|
+
return "max-h-[70vh]";
|
|
23
|
+
});
|
|
24
|
+
const pillClass = computed(() => {
|
|
25
|
+
if (props.panel.mode.value === "docked-left") return "left-3 top-3";
|
|
26
|
+
if (props.panel.mode.value === "docked-right") return "right-3 top-3";
|
|
27
|
+
return "";
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<Transition
|
|
33
|
+
enter-active-class="transition duration-150 ease-out"
|
|
34
|
+
enter-from-class="opacity-0 scale-95"
|
|
35
|
+
enter-to-class="opacity-100 scale-100"
|
|
36
|
+
leave-active-class="transition duration-100 ease-in"
|
|
37
|
+
leave-from-class="opacity-100 scale-100"
|
|
38
|
+
leave-to-class="opacity-0 scale-95"
|
|
39
|
+
>
|
|
40
|
+
<div
|
|
41
|
+
v-if="panel.collapsed.value"
|
|
42
|
+
key="pill"
|
|
43
|
+
class="absolute z-10"
|
|
44
|
+
:class="pillClass"
|
|
45
|
+
:style="panel.mode.value === 'floating' ? panel.floatingStyle.value : void 0"
|
|
46
|
+
>
|
|
47
|
+
<!-- Two-layer frame: outer glass ring, inner white pane — so the pill is
|
|
48
|
+
recognisably the same object as the panel it came from. -->
|
|
49
|
+
<div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
class="flex h-9 cursor-pointer items-center gap-1.5 rounded-lg bg-white px-3 text-sm font-medium transition hover:bg-muted/75"
|
|
53
|
+
:title="labels.expand"
|
|
54
|
+
@click="panel.setCollapsed(false)"
|
|
55
|
+
>
|
|
56
|
+
<slot name="pill-icon" />
|
|
57
|
+
{{ title }}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div
|
|
63
|
+
v-else
|
|
64
|
+
key="panel"
|
|
65
|
+
ref="shell"
|
|
66
|
+
class="absolute z-10 flex flex-col rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm"
|
|
67
|
+
:class="[shellClass, widthClass]"
|
|
68
|
+
:style="panel.mode.value === 'floating' ? panel.floatingStyle.value : void 0"
|
|
69
|
+
>
|
|
70
|
+
<div class="relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-white bg-white">
|
|
71
|
+
<div
|
|
72
|
+
class="flex shrink-0 touch-none select-none items-center gap-1 border-b border-black/[0.06] bg-white px-2 py-1.5"
|
|
73
|
+
:class="panel.dragging.value ? 'cursor-grabbing' : 'cursor-grab'"
|
|
74
|
+
@pointerdown="panel.startDrag($event, shell)"
|
|
75
|
+
>
|
|
76
|
+
<HamburgerMenu :size="14" class="shrink-0 text-muted-foreground/60" />
|
|
77
|
+
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ title }}</span>
|
|
78
|
+
|
|
79
|
+
<DropdownMenu>
|
|
80
|
+
<DropdownMenuTrigger as-child>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
84
|
+
:title="labels.placement"
|
|
85
|
+
>
|
|
86
|
+
<MenuDots :size="14" />
|
|
87
|
+
</button>
|
|
88
|
+
</DropdownMenuTrigger>
|
|
89
|
+
<DropdownMenuContent align="end" class="w-36 rounded-lg border border-black/[0.06] bg-white p-1 shadow-lg">
|
|
90
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="panel.dock('left')">{{ labels.dockLeft }}</DropdownMenuItem>
|
|
91
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="panel.dock('right')">{{ labels.dockRight }}</DropdownMenuItem>
|
|
92
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="panel.floatHere(shell)">{{ labels.floatHere }}</DropdownMenuItem>
|
|
93
|
+
<DropdownMenuSeparator class="my-1 h-px bg-black/[0.06]" />
|
|
94
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="panel.setCollapsed(true)">{{ labels.collapse }}</DropdownMenuItem>
|
|
95
|
+
</DropdownMenuContent>
|
|
96
|
+
</DropdownMenu>
|
|
97
|
+
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
101
|
+
:title="labels.collapse"
|
|
102
|
+
@click="panel.setCollapsed(true)"
|
|
103
|
+
>
|
|
104
|
+
<Minimize :size="14" />
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
109
|
+
<slot />
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div v-if="$slots.footer" class="shrink-0 border-t border-black/[0.06] px-3 py-2 text-xs text-muted-foreground">
|
|
113
|
+
<slot name="footer" />
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</Transition>
|
|
118
|
+
</template>
|