@revenexx/studio-shared 0.1.2 → 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.
Files changed (120) hide show
  1. package/README.md +22 -12
  2. package/dist/module.d.mts +15 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +58 -0
  5. package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +12 -0
  6. package/{components → dist/runtime/components}/DataTable/ColumnsMenu.vue +13 -19
  7. package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +12 -0
  8. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +87 -0
  9. package/dist/runtime/components/DataTable/DataTable.vue +510 -0
  10. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +87 -0
  11. package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +21 -0
  12. package/dist/runtime/components/DataTable/FilterMenu.vue +46 -0
  13. package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +21 -0
  14. package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -0
  15. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +189 -0
  16. package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -0
  17. package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +13 -0
  18. package/dist/runtime/components/DataTable/SortMenu.vue +47 -0
  19. package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +13 -0
  20. package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +20 -0
  21. package/dist/runtime/components/DataTable/ViewModeSwitch.vue +26 -0
  22. package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +20 -0
  23. package/dist/runtime/components/DataTable/types.d.ts +104 -0
  24. package/dist/runtime/components/DataTable/types.js +0 -0
  25. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +17 -0
  26. package/dist/runtime/components/Media/MediaFolderTree.vue +65 -0
  27. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +17 -0
  28. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +18 -0
  29. package/dist/runtime/components/Media/MediaPickerDialog.vue +163 -0
  30. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +18 -0
  31. package/dist/runtime/components/Media/MediaUploader.d.vue.ts +29 -0
  32. package/dist/runtime/components/Media/MediaUploader.vue +161 -0
  33. package/dist/runtime/components/Media/MediaUploader.vue.d.ts +29 -0
  34. package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +30 -0
  35. package/{components → dist/runtime/components}/Shared/BuildLogsFlyout.vue +37 -54
  36. package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +30 -0
  37. package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +17 -0
  38. package/dist/runtime/components/Shared/GitDeploymentSettings.vue +75 -0
  39. package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +17 -0
  40. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +13 -0
  41. package/{components → dist/runtime/components}/Shared/KpiTile.vue +11 -19
  42. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +13 -0
  43. package/dist/runtime/components/Shared/ListSurface.d.vue.ts +38 -0
  44. package/dist/runtime/components/Shared/ListSurface.vue +27 -0
  45. package/dist/runtime/components/Shared/ListSurface.vue.d.ts +38 -0
  46. package/dist/runtime/components/Shared/PageHeader.d.vue.ts +23 -0
  47. package/{components → dist/runtime/components}/Shared/PageHeader.vue +6 -8
  48. package/dist/runtime/components/Shared/PageHeader.vue.d.ts +23 -0
  49. package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +52 -0
  50. package/dist/runtime/components/Shared/RecordLayout.vue +63 -0
  51. package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +52 -0
  52. package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +26 -0
  53. package/dist/runtime/components/Shared/RecordRailCard.vue +20 -0
  54. package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +26 -0
  55. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +41 -0
  56. package/dist/runtime/components/Shared/SidebarNav.vue +48 -0
  57. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +41 -0
  58. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +15 -0
  59. package/dist/runtime/components/Shared/SidebarNavLink.vue +49 -0
  60. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +15 -0
  61. package/dist/runtime/components/Shared/SortHeader.d.vue.ts +23 -0
  62. package/dist/runtime/components/Shared/SortHeader.vue +35 -0
  63. package/dist/runtime/components/Shared/SortHeader.vue.d.ts +23 -0
  64. package/dist/runtime/components/Shared/StateCard.d.vue.ts +32 -0
  65. package/{components → dist/runtime/components}/Shared/StateCard.vue +21 -36
  66. package/dist/runtime/components/Shared/StateCard.vue.d.ts +32 -0
  67. package/dist/runtime/components/Shared/StudioCard.d.vue.ts +15 -0
  68. package/dist/runtime/components/Shared/StudioCard.vue +19 -0
  69. package/dist/runtime/components/Shared/StudioCard.vue.d.ts +15 -0
  70. package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -0
  71. package/{components → dist/runtime/components}/Shared/TabBar.vue +7 -16
  72. package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -0
  73. package/dist/runtime/components/Shared/ViewTab.d.vue.ts +7 -0
  74. package/{components → dist/runtime/components}/Shared/ViewTab.vue +7 -9
  75. package/dist/runtime/components/Shared/ViewTab.vue.d.ts +7 -0
  76. package/dist/runtime/components/Shared/Wizard.d.vue.ts +47 -0
  77. package/dist/runtime/components/Shared/Wizard.vue +66 -0
  78. package/dist/runtime/components/Shared/Wizard.vue.d.ts +47 -0
  79. package/dist/runtime/composables/useListRail.d.ts +9 -0
  80. package/dist/runtime/composables/useListRail.js +19 -0
  81. package/dist/runtime/composables/useMediaLibrary.d.ts +41 -0
  82. package/dist/runtime/composables/useMediaLibrary.js +36 -0
  83. package/dist/runtime/composables/usePlatformAuth.d.ts +35 -0
  84. package/dist/runtime/composables/usePlatformAuth.js +17 -0
  85. package/dist/runtime/composables/usePlatformTalkback.d.ts +29 -0
  86. package/dist/runtime/composables/usePlatformTalkback.js +54 -0
  87. package/dist/runtime/composables/usePlatformTenant.d.ts +14 -0
  88. package/dist/runtime/composables/usePlatformTenant.js +18 -0
  89. package/dist/runtime/composables/useT.d.ts +11 -0
  90. package/dist/runtime/composables/useT.js +8 -0
  91. package/dist/runtime/composables/useTalkbackChannels.d.ts +78 -0
  92. package/dist/runtime/composables/useTalkbackChannels.js +52 -0
  93. package/dist/runtime/lib/formatBridge.d.ts +50 -0
  94. package/dist/runtime/lib/formatBridge.js +54 -0
  95. package/dist/runtime/lib/i18nBridge.d.ts +31 -0
  96. package/dist/runtime/lib/i18nBridge.js +17 -0
  97. package/dist/runtime/lib/isoLists.d.ts +61 -0
  98. package/dist/runtime/lib/isoLists.js +62 -0
  99. package/dist/runtime/lib/sidebarNav.d.ts +34 -0
  100. package/dist/runtime/lib/sidebarNav.js +22 -0
  101. package/dist/runtime/lib/talkback.d.ts +59 -0
  102. package/dist/runtime/lib/talkback.js +49 -0
  103. package/dist/runtime/types/nav.d.ts +37 -0
  104. package/dist/runtime/types/nav.js +0 -0
  105. package/dist/runtime/types/storage.d.ts +67 -0
  106. package/dist/runtime/types/storage.js +0 -0
  107. package/dist/types.d.mts +27 -0
  108. package/package.json +41 -13
  109. package/components/DataTable/DataTable.vue +0 -518
  110. package/components/DataTable/FilterMenu.vue +0 -40
  111. package/components/DataTable/types.ts +0 -82
  112. package/components/Shared/GitDeploymentSettings.vue +0 -87
  113. package/components/Shared/SidebarNav.vue +0 -100
  114. package/components/Shared/SortHeader.vue +0 -50
  115. package/components/Shared/StudioCard.vue +0 -24
  116. package/components/Shared/Wizard.vue +0 -80
  117. package/composables/usePlatformAuth.ts +0 -35
  118. package/composables/usePlatformTenant.ts +0 -35
  119. package/index.ts +0 -16
  120. package/nuxt.config.ts +0 -23
@@ -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;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Minimal shape of a site deployment — just the fields this flyout renders.
3
+ * Kept local so studio-shared has no dependency on the private SDK; a full
4
+ * SDK `Models.Deployment` is structurally assignable to it.
5
+ */
6
+ interface BuildDeployment {
7
+ $id?: string;
8
+ status?: string;
9
+ buildLogs?: string;
10
+ buildDuration?: number;
11
+ providerBranch?: string;
12
+ providerCommitHash?: string;
13
+ providerCommitUrl?: string;
14
+ }
15
+ type __VLS_Props = {
16
+ open: boolean;
17
+ deployment: BuildDeployment | null;
18
+ loading: boolean;
19
+ isBuilding?: boolean;
20
+ userPinned?: boolean;
21
+ error?: string | null;
22
+ };
23
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
24
+ "update:open": (args_0: boolean) => any;
25
+ refresh: () => any;
26
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
27
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
28
+ onRefresh?: (() => any) | undefined;
29
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
+ export default _default;
@@ -1,63 +1,46 @@
1
- <script setup lang="ts">
2
- import { RefreshCircle as Loader2, Copy as CopyIcon } from '#solar-icons'
1
+ <script setup>
2
+ import { RefreshCircle as Loader2, Copy as CopyIcon } from "#solar-icons";
3
3
  import {
4
4
  Badge,
5
5
  Button,
6
- Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle,
7
- } from '@revenexx/studio'
8
- import { toast } from 'vue-sonner'
9
-
10
- /**
11
- * Minimal shape of a site deployment — just the fields this flyout renders.
12
- * Kept local so studio-shared has no dependency on the private SDK; a full
13
- * SDK `Models.Deployment` is structurally assignable to it.
14
- */
15
- interface BuildDeployment {
16
- $id?: string
17
- status?: string
18
- buildLogs?: string
19
- buildDuration?: number
20
- providerBranch?: string
21
- providerCommitHash?: string
22
- providerCommitUrl?: string
23
- }
24
-
25
- defineProps<{
26
- open: boolean
27
- deployment: BuildDeployment | null
28
- loading: boolean
29
- isBuilding?: boolean
30
- userPinned?: boolean
31
- error?: string | null
32
- }>()
33
-
34
- const emit = defineEmits<{
35
- 'update:open': [boolean]
36
- 'refresh': []
37
- }>()
38
-
39
- async function copyId(id: string | undefined) {
40
- if (!id) return
6
+ Sheet,
7
+ SheetContent,
8
+ SheetDescription,
9
+ SheetHeader,
10
+ SheetTitle
11
+ } from "@revenexx/studio";
12
+ import { toast } from "vue-sonner";
13
+ defineProps({
14
+ open: { type: Boolean, required: true },
15
+ deployment: { type: [Object, null], required: true },
16
+ loading: { type: Boolean, required: true },
17
+ isBuilding: { type: Boolean, required: false },
18
+ userPinned: { type: Boolean, required: false },
19
+ error: { type: [String, null], required: false }
20
+ });
21
+ const emit = defineEmits(["update:open", "refresh"]);
22
+ const t = useT();
23
+ async function copyId(id) {
24
+ if (!id) return;
41
25
  try {
42
- await navigator.clipboard.writeText(id)
43
- toast.success('ID copied')
44
- }
45
- catch {
46
- toast.error('Could not copy')
26
+ await navigator.clipboard.writeText(id);
27
+ toast.success(t("shared.buildlogs.toast.idcopied"));
28
+ } catch {
29
+ toast.error(t("shared.buildlogs.toast.copyfailed"));
47
30
  }
48
31
  }
49
32
  </script>
50
33
 
51
34
  <template>
52
- <Sheet :open="open" @update:open="(v: boolean) => emit('update:open', v)">
35
+ <Sheet :open="open" @update:open="(v) => emit('update:open', v)">
53
36
  <SheetContent side="right" class="flex w-full flex-col gap-0 p-0 sm:max-w-[max(50vw,640px)]">
54
37
  <SheetHeader class="border-b px-4 py-3">
55
38
  <div class="flex items-center gap-2">
56
- <Badge variant="outline" class="shrink-0">Build</Badge>
39
+ <Badge variant="outline" class="shrink-0">{{ t("shared.buildlogs.badge.build") }}</Badge>
57
40
  <SheetTitle class="truncate font-mono text-sm">
58
- {{ deployment?.$id || 'Build logs' }}
41
+ {{ deployment?.$id || t("shared.buildlogs.title.default") }}
59
42
  </SheetTitle>
60
- <button v-if="deployment?.$id" title="Copy ID" class="ml-auto" @click="copyId(deployment.$id)">
43
+ <button v-if="deployment?.$id" :title="t('shared.buildlogs.tooltip.copyid')" class="ml-auto" @click="copyId(deployment.$id)">
61
44
  <CopyIcon :size="12" class="text-muted-foreground hover:text-foreground" />
62
45
  </button>
63
46
  </div>
@@ -76,29 +59,29 @@ async function copyId(id: string | undefined) {
76
59
  </template>
77
60
  <template v-if="userPinned">
78
61
  <span class="text-muted-foreground">·</span>
79
- <span>pinned</span>
62
+ <span>{{ t("shared.buildlogs.badge.pinned") }}</span>
80
63
  </template>
81
64
  </SheetDescription>
82
65
  </SheetHeader>
83
66
 
84
67
  <div class="flex-1 overflow-y-auto">
85
68
  <div v-if="loading && !deployment" class="py-8 text-center text-sm text-muted-foreground">
86
- <Loader2 :size="16" class="mr-2 inline animate-spin" /> Loading…
69
+ <Loader2 :size="16" class="mr-2 inline animate-spin" /> {{ t("shared.buildlogs.loading") }}
87
70
  </div>
88
71
  <p v-else-if="error" class="p-4 text-sm text-rose-700">{{ error }}</p>
89
72
 
90
73
  <template v-else-if="deployment">
91
74
  <div class="flex items-center justify-between gap-2 px-4 py-2 border-b bg-muted/30">
92
75
  <span class="text-xs text-muted-foreground">
93
- Status: <span class="font-medium text-foreground">{{ deployment.status || '—' }}</span>
76
+ {{ t("shared.buildlogs.status.label") }} <span class="font-medium text-foreground">{{ deployment.status || "\u2014" }}</span>
94
77
  <template v-if="deployment.buildDuration">
95
- · {{ deployment.buildDuration }}s
78
+ · {{ deployment.buildDuration }}{{ t("shared.buildlogs.duration.unit") }}
96
79
  </template>
97
80
  </span>
98
81
  <div class="flex items-center gap-2">
99
82
  <Loader2 v-if="isBuilding || loading" :size="12" class="animate-spin text-muted-foreground" />
100
83
  <Button variant="ghost" size="sm" class="h-6 text-xs" @click="emit('refresh')">
101
- Refresh
84
+ {{ t("shared.buildlogs.action.refresh") }}
102
85
  </Button>
103
86
  </div>
104
87
  </div>
@@ -110,16 +93,16 @@ async function copyId(id: string | undefined) {
110
93
  >{{ stripAnsi(deployment.buildLogs) }}</pre>
111
94
  <p v-else-if="isBuilding" class="flex items-center gap-2 text-sm text-muted-foreground py-8">
112
95
  <Loader2 :size="12" class="animate-spin" />
113
- Waiting for first log output…
96
+ {{ t("shared.buildlogs.waiting") }}
114
97
  </p>
115
98
  <p v-else class="text-sm text-muted-foreground py-8 text-center">
116
- No logs captured for this build.
99
+ {{ t("shared.buildlogs.nologs") }}
117
100
  </p>
118
101
  </div>
119
102
  </template>
120
103
 
121
104
  <div v-else class="py-12 text-center text-sm text-muted-foreground">
122
- Select a build from the list to see its logs.
105
+ {{ t("shared.buildlogs.selectbuild") }}
123
106
  </div>
124
107
  </div>
125
108
  </SheetContent>
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Minimal shape of a site deployment — just the fields this flyout renders.
3
+ * Kept local so studio-shared has no dependency on the private SDK; a full
4
+ * SDK `Models.Deployment` is structurally assignable to it.
5
+ */
6
+ interface BuildDeployment {
7
+ $id?: string;
8
+ status?: string;
9
+ buildLogs?: string;
10
+ buildDuration?: number;
11
+ providerBranch?: string;
12
+ providerCommitHash?: string;
13
+ providerCommitUrl?: string;
14
+ }
15
+ type __VLS_Props = {
16
+ open: boolean;
17
+ deployment: BuildDeployment | null;
18
+ loading: boolean;
19
+ isBuilding?: boolean;
20
+ userPinned?: boolean;
21
+ error?: string | null;
22
+ };
23
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
24
+ "update:open": (args_0: boolean) => any;
25
+ refresh: () => any;
26
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
27
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
28
+ onRefresh?: (() => any) | undefined;
29
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
+ export default _default;
@@ -0,0 +1,17 @@
1
+ type __VLS_Props = {
2
+ triggerMode: string;
3
+ previewsEnabled: boolean;
4
+ disabled?: boolean;
5
+ showPreviews?: boolean;
6
+ };
7
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ "update:triggerMode": (value: string) => any;
9
+ "update:previewsEnabled": (value: boolean) => any;
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
11
+ "onUpdate:triggerMode"?: ((value: string) => any) | undefined;
12
+ "onUpdate:previewsEnabled"?: ((value: boolean) => any) | undefined;
13
+ }>, {
14
+ disabled: boolean;
15
+ showPreviews: boolean;
16
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
+ export default _default;