@revenexx/studio-shared 0.2.0 → 0.3.0
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 +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +32 -1
- package/dist/runtime/components/DataTable/ColumnsMenu.vue +3 -2
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +32 -4
- package/dist/runtime/components/DataTable/DataTable.vue +91 -34
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +32 -4
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +12 -3
- package/dist/runtime/components/DataTable/FilterMenu.vue +20 -10
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +12 -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 +34 -0
- 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/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/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 +78 -0
- package/dist/runtime/composables/useTalkbackChannels.js +52 -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 +31 -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 +12 -0
- package/package.json +18 -2
|
@@ -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>
|
|
@@ -20,38 +20,39 @@ defineProps({
|
|
|
20
20
|
showPreviews: { type: Boolean, required: false, default: true }
|
|
21
21
|
});
|
|
22
22
|
defineEmits(["update:triggerMode", "update:previewsEnabled"]);
|
|
23
|
+
const t = useT();
|
|
23
24
|
</script>
|
|
24
25
|
|
|
25
26
|
<template>
|
|
26
27
|
<Card class="bg-white/60 backdrop-blur-sm">
|
|
27
28
|
<CardHeader>
|
|
28
|
-
<CardTitle class="text-sm">
|
|
29
|
+
<CardTitle class="text-sm">{{ t("shared.git.settings.title") }}</CardTitle>
|
|
29
30
|
<CardDescription class="text-sm">
|
|
30
|
-
|
|
31
|
+
{{ t("shared.git.settings.description") }}
|
|
31
32
|
</CardDescription>
|
|
32
33
|
</CardHeader>
|
|
33
34
|
<CardContent class="space-y-4">
|
|
34
35
|
<div class="space-y-1">
|
|
35
|
-
<Label for="vcs-trigger-mode">
|
|
36
|
+
<Label for="vcs-trigger-mode">{{ t("shared.git.settings.field.when") }}</Label>
|
|
36
37
|
<Select
|
|
37
38
|
:model-value="triggerMode"
|
|
38
39
|
:disabled="disabled"
|
|
39
40
|
@update:model-value="(v) => $emit('update:triggerMode', v)"
|
|
40
41
|
>
|
|
41
42
|
<SelectTrigger id="vcs-trigger-mode">
|
|
42
|
-
<SelectValue placeholder="
|
|
43
|
+
<SelectValue :placeholder="t('shared.git.settings.select.placeholder')" />
|
|
43
44
|
</SelectTrigger>
|
|
44
45
|
<SelectContent>
|
|
45
|
-
<SelectItem value="push">
|
|
46
|
-
<SelectItem value="tag">
|
|
46
|
+
<SelectItem value="push">{{ t("shared.git.settings.option.push") }}</SelectItem>
|
|
47
|
+
<SelectItem value="tag">{{ t("shared.git.settings.option.tag") }}</SelectItem>
|
|
47
48
|
</SelectContent>
|
|
48
49
|
</Select>
|
|
49
50
|
<p class="text-sm text-muted-foreground">
|
|
50
51
|
<template v-if="triggerMode === 'tag'">
|
|
51
|
-
|
|
52
|
+
{{ t("shared.git.settings.hint.tag") }}
|
|
52
53
|
</template>
|
|
53
54
|
<template v-else>
|
|
54
|
-
|
|
55
|
+
{{ t("shared.git.settings.hint.push") }}
|
|
55
56
|
</template>
|
|
56
57
|
</p>
|
|
57
58
|
</div>
|
|
@@ -63,9 +64,9 @@ defineEmits(["update:triggerMode", "update:previewsEnabled"]);
|
|
|
63
64
|
@update:model-value="(v) => $emit('update:previewsEnabled', v)"
|
|
64
65
|
/>
|
|
65
66
|
<div class="text-sm">
|
|
66
|
-
<div class="font-medium">
|
|
67
|
+
<div class="font-medium">{{ t("shared.git.settings.previews.title") }}</div>
|
|
67
68
|
<div class="text-sm text-muted-foreground">
|
|
68
|
-
|
|
69
|
+
{{ t("shared.git.settings.previews.description") }}
|
|
69
70
|
</div>
|
|
70
71
|
</div>
|
|
71
72
|
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame every list surface with a left rail shares: an optional,
|
|
3
|
+
* collapsible rail column, the list itself, and an optional aside (a detail
|
|
4
|
+
* flyout column).
|
|
5
|
+
*
|
|
6
|
+
* The collapse *control* lives in the DataTable toolbar (`rail` prop +
|
|
7
|
+
* `v-model:rail-open`) so a surface never grows a second control row; this
|
|
8
|
+
* component only reacts to the same boolean. Use `useListRail()` for a state
|
|
9
|
+
* that survives a reload.
|
|
10
|
+
*/
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
/** Rail visible? Bind the same ref the DataTable's `rail-open` model has. */
|
|
13
|
+
railOpen?: boolean;
|
|
14
|
+
/** Rail track width (default 240px). */
|
|
15
|
+
railWidth?: string;
|
|
16
|
+
/** Aside track width (default 280px). */
|
|
17
|
+
asideWidth?: string;
|
|
18
|
+
};
|
|
19
|
+
declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {};
|
|
20
|
+
type __VLS_Slots = {} & {
|
|
21
|
+
rail?: (props: typeof __VLS_1) => any;
|
|
22
|
+
} & {
|
|
23
|
+
default?: (props: typeof __VLS_3) => any;
|
|
24
|
+
} & {
|
|
25
|
+
aside?: (props: typeof __VLS_5) => any;
|
|
26
|
+
};
|
|
27
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
28
|
+
railOpen: boolean;
|
|
29
|
+
railWidth: string;
|
|
30
|
+
asideWidth: string;
|
|
31
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
32
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
33
|
+
export default _default;
|
|
34
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
35
|
+
new (): {
|
|
36
|
+
$slots: S;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
railOpen: { type: Boolean, required: false, default: true },
|
|
4
|
+
railWidth: { type: String, required: false, default: "240px" },
|
|
5
|
+
asideWidth: { type: String, required: false, default: "280px" }
|
|
6
|
+
});
|
|
7
|
+
const slots = useSlots();
|
|
8
|
+
const hasAside = computed(() => !!slots.aside);
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div
|
|
13
|
+
class="grid grid-cols-1 items-start gap-3"
|
|
14
|
+
:style="{ '--rail-w': railWidth, '--aside-w': asideWidth }"
|
|
15
|
+
:class="railOpen ? hasAside ? 'lg:grid-cols-[var(--rail-w)_minmax(0,1fr)_var(--aside-w)]' : 'lg:grid-cols-[var(--rail-w)_minmax(0,1fr)]' : hasAside ? 'lg:grid-cols-[minmax(0,1fr)_var(--aside-w)]' : 'lg:grid-cols-1'"
|
|
16
|
+
>
|
|
17
|
+
<div v-if="railOpen" class="min-w-0">
|
|
18
|
+
<slot name="rail" />
|
|
19
|
+
</div>
|
|
20
|
+
<div class="min-w-0">
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
<div v-if="hasAside" class="min-w-0">
|
|
24
|
+
<slot name="aside" />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame every list surface with a left rail shares: an optional,
|
|
3
|
+
* collapsible rail column, the list itself, and an optional aside (a detail
|
|
4
|
+
* flyout column).
|
|
5
|
+
*
|
|
6
|
+
* The collapse *control* lives in the DataTable toolbar (`rail` prop +
|
|
7
|
+
* `v-model:rail-open`) so a surface never grows a second control row; this
|
|
8
|
+
* component only reacts to the same boolean. Use `useListRail()` for a state
|
|
9
|
+
* that survives a reload.
|
|
10
|
+
*/
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
/** Rail visible? Bind the same ref the DataTable's `rail-open` model has. */
|
|
13
|
+
railOpen?: boolean;
|
|
14
|
+
/** Rail track width (default 240px). */
|
|
15
|
+
railWidth?: string;
|
|
16
|
+
/** Aside track width (default 280px). */
|
|
17
|
+
asideWidth?: string;
|
|
18
|
+
};
|
|
19
|
+
declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {};
|
|
20
|
+
type __VLS_Slots = {} & {
|
|
21
|
+
rail?: (props: typeof __VLS_1) => any;
|
|
22
|
+
} & {
|
|
23
|
+
default?: (props: typeof __VLS_3) => any;
|
|
24
|
+
} & {
|
|
25
|
+
aside?: (props: typeof __VLS_5) => any;
|
|
26
|
+
};
|
|
27
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
28
|
+
railOpen: boolean;
|
|
29
|
+
railWidth: string;
|
|
30
|
+
asideWidth: string;
|
|
31
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
32
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
33
|
+
export default _default;
|
|
34
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
35
|
+
new (): {
|
|
36
|
+
$slots: S;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface RecordTab {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
badge?: number | string | null;
|
|
5
|
+
}
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
/** Tabs for the body column. Empty = render no tab bar. */
|
|
8
|
+
tabs?: RecordTab[];
|
|
9
|
+
/** Active tab key (`v-model`). */
|
|
10
|
+
modelValue?: string;
|
|
11
|
+
/** Rail track width. 320px reads as a column; below ~280 it reads as a list. */
|
|
12
|
+
railWidth?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Height of the sticky PageHeader, in px — the rail (and the shared TabBar)
|
|
15
|
+
* park directly under it. 57 = `px-4 pt-4 pb-3` + the text-xl line + hairline;
|
|
16
|
+
* pass something else only together with a custom `#tabs` slot, because
|
|
17
|
+
* `TabBar` hard-codes the same offset.
|
|
18
|
+
*/
|
|
19
|
+
stickyOffset?: number;
|
|
20
|
+
/** What the narrow-width disclosure calls the rail body. */
|
|
21
|
+
railLabel?: string;
|
|
22
|
+
/** Start the narrow-width disclosure expanded (initial value only). */
|
|
23
|
+
railOpenOnNarrow?: boolean;
|
|
24
|
+
};
|
|
25
|
+
declare var __VLS_1: {}, __VLS_7: {}, __VLS_9: {}, __VLS_18: {};
|
|
26
|
+
type __VLS_Slots = {} & {
|
|
27
|
+
'rail-head'?: (props: typeof __VLS_1) => any;
|
|
28
|
+
} & {
|
|
29
|
+
rail?: (props: typeof __VLS_7) => any;
|
|
30
|
+
} & {
|
|
31
|
+
tabs?: (props: typeof __VLS_9) => any;
|
|
32
|
+
} & {
|
|
33
|
+
default?: (props: typeof __VLS_18) => any;
|
|
34
|
+
};
|
|
35
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
36
|
+
"update:modelValue": (key: string) => any;
|
|
37
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
38
|
+
"onUpdate:modelValue"?: ((key: string) => any) | undefined;
|
|
39
|
+
}>, {
|
|
40
|
+
modelValue: string;
|
|
41
|
+
railWidth: string;
|
|
42
|
+
tabs: RecordTab[];
|
|
43
|
+
stickyOffset: number;
|
|
44
|
+
railOpenOnNarrow: boolean;
|
|
45
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
46
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
47
|
+
export default _default;
|
|
48
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
49
|
+
new (): {
|
|
50
|
+
$slots: S;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { AltArrowDown } from "#solar-icons";
|
|
3
|
+
import TabBar from "./TabBar.vue";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
tabs: { type: Array, required: false, default: () => [] },
|
|
6
|
+
modelValue: { type: String, required: false, default: "" },
|
|
7
|
+
railWidth: { type: String, required: false, default: "20rem" },
|
|
8
|
+
stickyOffset: { type: Number, required: false, default: 57 },
|
|
9
|
+
railLabel: { type: String, required: false },
|
|
10
|
+
railOpenOnNarrow: { type: Boolean, required: false, default: false }
|
|
11
|
+
});
|
|
12
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
13
|
+
const t = useT();
|
|
14
|
+
const railLabelText = computed(() => props.railLabel ?? t("shared.recordlayout.rail.label.default"));
|
|
15
|
+
const narrowOpen = ref(props.railOpenOnNarrow);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div
|
|
20
|
+
class="grid grid-cols-1 items-start gap-4 xl:grid-cols-[var(--record-rail-w)_minmax(0,1fr)]"
|
|
21
|
+
:style="{ '--record-rail-w': railWidth, '--record-rail-top': `${stickyOffset}px` }"
|
|
22
|
+
>
|
|
23
|
+
<aside
|
|
24
|
+
class="min-w-0 space-y-3 xl:sticky xl:top-[var(--record-rail-top)] xl:max-h-[calc(100svh_-_var(--record-rail-top)_-_5.5rem)] xl:overflow-y-auto xl:overscroll-contain xl:pb-1"
|
|
25
|
+
>
|
|
26
|
+
<slot name="rail-head" />
|
|
27
|
+
|
|
28
|
+
<!-- Narrow widths: the rail body folds away so the tabs stay reachable. -->
|
|
29
|
+
<button
|
|
30
|
+
v-if="$slots.rail"
|
|
31
|
+
type="button"
|
|
32
|
+
class="flex w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-black/[0.06] bg-white/60 px-3 py-2 text-sm font-medium text-[#0B0522] transition-colors hover:text-primary xl:hidden"
|
|
33
|
+
:aria-expanded="narrowOpen"
|
|
34
|
+
@click="narrowOpen = !narrowOpen"
|
|
35
|
+
>
|
|
36
|
+
<span>{{ narrowOpen ? t("shared.recordlayout.rail.action.hide", { label: railLabelText }) : t("shared.recordlayout.rail.action.show", { label: railLabelText }) }}</span>
|
|
37
|
+
<AltArrowDown
|
|
38
|
+
:size="14"
|
|
39
|
+
class="shrink-0 transition-transform"
|
|
40
|
+
:class="narrowOpen ? 'rotate-180' : ''"
|
|
41
|
+
/>
|
|
42
|
+
</button>
|
|
43
|
+
|
|
44
|
+
<div :class="narrowOpen ? 'space-y-3' : 'hidden space-y-3 xl:block'">
|
|
45
|
+
<slot name="rail" />
|
|
46
|
+
</div>
|
|
47
|
+
</aside>
|
|
48
|
+
|
|
49
|
+
<div class="min-w-0">
|
|
50
|
+
<slot name="tabs">
|
|
51
|
+
<TabBar
|
|
52
|
+
v-if="tabs.length"
|
|
53
|
+
:tabs="tabs"
|
|
54
|
+
:model-value="modelValue"
|
|
55
|
+
@update:model-value="(k) => emit('update:modelValue', k)"
|
|
56
|
+
/>
|
|
57
|
+
</slot>
|
|
58
|
+
<div class="pt-4">
|
|
59
|
+
<slot />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface RecordTab {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
badge?: number | string | null;
|
|
5
|
+
}
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
/** Tabs for the body column. Empty = render no tab bar. */
|
|
8
|
+
tabs?: RecordTab[];
|
|
9
|
+
/** Active tab key (`v-model`). */
|
|
10
|
+
modelValue?: string;
|
|
11
|
+
/** Rail track width. 320px reads as a column; below ~280 it reads as a list. */
|
|
12
|
+
railWidth?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Height of the sticky PageHeader, in px — the rail (and the shared TabBar)
|
|
15
|
+
* park directly under it. 57 = `px-4 pt-4 pb-3` + the text-xl line + hairline;
|
|
16
|
+
* pass something else only together with a custom `#tabs` slot, because
|
|
17
|
+
* `TabBar` hard-codes the same offset.
|
|
18
|
+
*/
|
|
19
|
+
stickyOffset?: number;
|
|
20
|
+
/** What the narrow-width disclosure calls the rail body. */
|
|
21
|
+
railLabel?: string;
|
|
22
|
+
/** Start the narrow-width disclosure expanded (initial value only). */
|
|
23
|
+
railOpenOnNarrow?: boolean;
|
|
24
|
+
};
|
|
25
|
+
declare var __VLS_1: {}, __VLS_7: {}, __VLS_9: {}, __VLS_18: {};
|
|
26
|
+
type __VLS_Slots = {} & {
|
|
27
|
+
'rail-head'?: (props: typeof __VLS_1) => any;
|
|
28
|
+
} & {
|
|
29
|
+
rail?: (props: typeof __VLS_7) => any;
|
|
30
|
+
} & {
|
|
31
|
+
tabs?: (props: typeof __VLS_9) => any;
|
|
32
|
+
} & {
|
|
33
|
+
default?: (props: typeof __VLS_18) => any;
|
|
34
|
+
};
|
|
35
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
36
|
+
"update:modelValue": (key: string) => any;
|
|
37
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
38
|
+
"onUpdate:modelValue"?: ((key: string) => any) | undefined;
|
|
39
|
+
}>, {
|
|
40
|
+
modelValue: string;
|
|
41
|
+
railWidth: string;
|
|
42
|
+
tabs: RecordTab[];
|
|
43
|
+
stickyOffset: number;
|
|
44
|
+
railOpenOnNarrow: boolean;
|
|
45
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
46
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
47
|
+
export default _default;
|
|
48
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
49
|
+
new (): {
|
|
50
|
+
$slots: S;
|
|
51
|
+
};
|
|
52
|
+
};
|