@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,189 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { AltArrowDown, Bookmark, TrashBinMinimalistic } from "#solar-icons";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
DropdownMenu,
|
|
12
|
+
DropdownMenuContent,
|
|
13
|
+
DropdownMenuItem,
|
|
14
|
+
DropdownMenuSeparator,
|
|
15
|
+
DropdownMenuTrigger,
|
|
16
|
+
Input
|
|
17
|
+
} from "@revenexx/studio";
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
config: { type: Object, required: true }
|
|
20
|
+
});
|
|
21
|
+
const t = useT();
|
|
22
|
+
const views = ref([]);
|
|
23
|
+
function read() {
|
|
24
|
+
try {
|
|
25
|
+
if (typeof window === "undefined") return [];
|
|
26
|
+
const raw = window.localStorage.getItem(props.config.storageKey);
|
|
27
|
+
if (!raw) return [];
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (!Array.isArray(parsed)) return [];
|
|
30
|
+
return parsed.filter((v) => !!v && typeof v === "object" && typeof v.name === "string");
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function persist() {
|
|
36
|
+
try {
|
|
37
|
+
if (typeof window === "undefined") return;
|
|
38
|
+
window.localStorage.setItem(props.config.storageKey, JSON.stringify(views.value));
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
onMounted(() => {
|
|
43
|
+
views.value = read();
|
|
44
|
+
});
|
|
45
|
+
watch(() => props.config.storageKey, () => {
|
|
46
|
+
views.value = read();
|
|
47
|
+
});
|
|
48
|
+
const allLabel = computed(() => props.config.allLabel ?? t("shared.datatable.savedviews.all.default"));
|
|
49
|
+
function same(a, b) {
|
|
50
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
51
|
+
}
|
|
52
|
+
const isDefault = computed(() => same(props.config.snapshot(), props.config.defaultSnapshot()));
|
|
53
|
+
function isActive(v) {
|
|
54
|
+
return same(v.snapshot, props.config.snapshot());
|
|
55
|
+
}
|
|
56
|
+
const activeLabel = computed(() => {
|
|
57
|
+
const hit = views.value.find(isActive);
|
|
58
|
+
if (hit) return hit.name;
|
|
59
|
+
return isDefault.value ? allLabel.value : t("shared.datatable.savedviews.custom");
|
|
60
|
+
});
|
|
61
|
+
function applyDefault() {
|
|
62
|
+
props.config.apply(props.config.defaultSnapshot());
|
|
63
|
+
}
|
|
64
|
+
function applyView(v) {
|
|
65
|
+
props.config.apply(v.snapshot);
|
|
66
|
+
}
|
|
67
|
+
const saveOpen = ref(false);
|
|
68
|
+
const saveName = ref("");
|
|
69
|
+
function openSave() {
|
|
70
|
+
saveName.value = "";
|
|
71
|
+
saveOpen.value = true;
|
|
72
|
+
}
|
|
73
|
+
function save() {
|
|
74
|
+
const name = saveName.value.trim();
|
|
75
|
+
if (!name) return;
|
|
76
|
+
const entry = { name, snapshot: props.config.snapshot() };
|
|
77
|
+
const idx = views.value.findIndex((v) => v.name === name);
|
|
78
|
+
if (idx >= 0) views.value.splice(idx, 1, entry);
|
|
79
|
+
else views.value.push(entry);
|
|
80
|
+
persist();
|
|
81
|
+
saveOpen.value = false;
|
|
82
|
+
}
|
|
83
|
+
const deleteTarget = ref(null);
|
|
84
|
+
function remove() {
|
|
85
|
+
if (!deleteTarget.value) return;
|
|
86
|
+
views.value = views.value.filter((v) => v.name !== deleteTarget.value.name);
|
|
87
|
+
persist();
|
|
88
|
+
deleteTarget.value = null;
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<template>
|
|
93
|
+
<DropdownMenu>
|
|
94
|
+
<DropdownMenuTrigger as-child>
|
|
95
|
+
<Button
|
|
96
|
+
variant="outline"
|
|
97
|
+
size="sm"
|
|
98
|
+
class="h-7 max-w-48 gap-1.5 text-sm"
|
|
99
|
+
:class="isDefault ? '' : 'border-primary/30 text-primary'"
|
|
100
|
+
:aria-label="t('shared.datatable.savedviews.aria')"
|
|
101
|
+
>
|
|
102
|
+
<Bookmark :size="13" />
|
|
103
|
+
<span class="truncate">{{ activeLabel }}</span>
|
|
104
|
+
<AltArrowDown :size="12" class="opacity-60" />
|
|
105
|
+
</Button>
|
|
106
|
+
</DropdownMenuTrigger>
|
|
107
|
+
<DropdownMenuContent align="end" class="w-56">
|
|
108
|
+
<DropdownMenuItem class="gap-2" @select="applyDefault">
|
|
109
|
+
<span class="flex-1 truncate">{{ allLabel }}</span>
|
|
110
|
+
<span v-if="isDefault" class="text-xs text-primary">{{ t("shared.datatable.savedviews.active.badge") }}</span>
|
|
111
|
+
</DropdownMenuItem>
|
|
112
|
+
<template v-if="views.length">
|
|
113
|
+
<DropdownMenuSeparator />
|
|
114
|
+
<DropdownMenuItem
|
|
115
|
+
v-for="v in views"
|
|
116
|
+
:key="v.name"
|
|
117
|
+
class="gap-2"
|
|
118
|
+
@select="applyView(v)"
|
|
119
|
+
>
|
|
120
|
+
<span class="flex-1 truncate">{{ v.name }}</span>
|
|
121
|
+
<span
|
|
122
|
+
role="button"
|
|
123
|
+
class="text-muted-foreground transition-colors hover:text-rose-600"
|
|
124
|
+
:aria-label="t('shared.datatable.savedviews.delete.aria', { name: v.name })"
|
|
125
|
+
@click.stop="deleteTarget = v"
|
|
126
|
+
@pointerdown.stop.prevent
|
|
127
|
+
>
|
|
128
|
+
<TrashBinMinimalistic :size="13" />
|
|
129
|
+
</span>
|
|
130
|
+
</DropdownMenuItem>
|
|
131
|
+
</template>
|
|
132
|
+
<DropdownMenuSeparator />
|
|
133
|
+
<DropdownMenuItem @select="openSave">
|
|
134
|
+
{{ t("shared.datatable.savedviews.action.savecurrent") }}
|
|
135
|
+
</DropdownMenuItem>
|
|
136
|
+
</DropdownMenuContent>
|
|
137
|
+
</DropdownMenu>
|
|
138
|
+
|
|
139
|
+
<!-- Save the current filters + sort under a name -->
|
|
140
|
+
<Dialog :open="saveOpen" @update:open="(v2) => {
|
|
141
|
+
if (!v2) saveOpen = false;
|
|
142
|
+
}">
|
|
143
|
+
<DialogContent>
|
|
144
|
+
<DialogHeader>
|
|
145
|
+
<DialogTitle>{{ t("shared.datatable.savedviews.savedialog.title") }}</DialogTitle>
|
|
146
|
+
<DialogDescription>{{ t("shared.datatable.savedviews.savedialog.description") }}</DialogDescription>
|
|
147
|
+
</DialogHeader>
|
|
148
|
+
<div class="space-y-1">
|
|
149
|
+
<label for="saved-view-name" class="text-sm font-medium">{{ t("shared.datatable.savedviews.savedialog.field.name.label") }}</label>
|
|
150
|
+
<Input
|
|
151
|
+
id="saved-view-name"
|
|
152
|
+
v-model="saveName"
|
|
153
|
+
:placeholder="t('shared.datatable.savedviews.savedialog.field.name.placeholder')"
|
|
154
|
+
@keydown.enter="save"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
<DialogFooter>
|
|
158
|
+
<Button variant="outline" @click="saveOpen = false">
|
|
159
|
+
{{ t("shared.datatable.savedviews.savedialog.action.cancel") }}
|
|
160
|
+
</Button>
|
|
161
|
+
<Button :disabled="!saveName.trim()" @click="save">
|
|
162
|
+
{{ t("shared.datatable.savedviews.savedialog.action.save") }}
|
|
163
|
+
</Button>
|
|
164
|
+
</DialogFooter>
|
|
165
|
+
</DialogContent>
|
|
166
|
+
</Dialog>
|
|
167
|
+
|
|
168
|
+
<!-- Delete a saved view -->
|
|
169
|
+
<Dialog :open="deleteTarget !== null" @update:open="(v2) => {
|
|
170
|
+
if (!v2) deleteTarget = null;
|
|
171
|
+
}">
|
|
172
|
+
<DialogContent>
|
|
173
|
+
<DialogHeader>
|
|
174
|
+
<DialogTitle>{{ t("shared.datatable.savedviews.deletedialog.title") }}</DialogTitle>
|
|
175
|
+
<DialogDescription>
|
|
176
|
+
{{ t("shared.datatable.savedviews.deletedialog.description", { name: deleteTarget?.name ?? "" }) }}
|
|
177
|
+
</DialogDescription>
|
|
178
|
+
</DialogHeader>
|
|
179
|
+
<DialogFooter>
|
|
180
|
+
<Button variant="outline" @click="deleteTarget = null">
|
|
181
|
+
{{ t("shared.datatable.savedviews.deletedialog.action.cancel") }}
|
|
182
|
+
</Button>
|
|
183
|
+
<Button variant="outline" class="border-rose-500/30 text-rose-700 hover:bg-rose-500/10" @click="remove">
|
|
184
|
+
{{ t("shared.datatable.savedviews.deletedialog.action.delete") }}
|
|
185
|
+
</Button>
|
|
186
|
+
</DialogFooter>
|
|
187
|
+
</DialogContent>
|
|
188
|
+
</Dialog>
|
|
189
|
+
</template>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DataTableSavedViews } from './types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
config: DataTableSavedViews;
|
|
4
|
+
};
|
|
5
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DataTableColumn, SortState } from './types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
columns: DataTableColumn[];
|
|
4
|
+
};
|
|
5
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
6
|
+
modelValue: SortState;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:modelValue": (value: SortState) => any;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
11
|
+
"onUpdate:modelValue"?: ((value: SortState) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { AltArrowDown, SortVertical } from "#solar-icons";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger
|
|
9
|
+
} from "@revenexx/studio";
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
columns: { type: Array, required: true }
|
|
12
|
+
});
|
|
13
|
+
const sort = defineModel({ type: Object, ...{ required: true } });
|
|
14
|
+
const t = useT();
|
|
15
|
+
const label = computed(() => {
|
|
16
|
+
const col = props.columns.find((c) => c.key === sort.value.column);
|
|
17
|
+
if (!col || !sort.value.direction) return t("shared.datatable.sortmenu.label.default");
|
|
18
|
+
return `${col.label} ${sort.value.direction === "asc" ? "\u2191" : "\u2193"}`;
|
|
19
|
+
});
|
|
20
|
+
function pick(key, direction) {
|
|
21
|
+
sort.value = { column: key, direction };
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<DropdownMenu>
|
|
27
|
+
<DropdownMenuTrigger as-child>
|
|
28
|
+
<Button variant="outline" size="sm" class="h-7 gap-1.5 text-sm" :aria-label="t('shared.datatable.sortmenu.aria')">
|
|
29
|
+
<SortVertical :size="14" />
|
|
30
|
+
{{ label }}
|
|
31
|
+
<AltArrowDown :size="12" class="opacity-60" />
|
|
32
|
+
</Button>
|
|
33
|
+
</DropdownMenuTrigger>
|
|
34
|
+
<DropdownMenuContent align="start" class="w-52">
|
|
35
|
+
<template v-for="col in columns" :key="col.key">
|
|
36
|
+
<DropdownMenuItem class="gap-2" @select="pick(col.key, 'asc')">
|
|
37
|
+
<span class="flex-1 truncate">{{ col.label }} ↑</span>
|
|
38
|
+
<span v-if="sort.column === col.key && sort.direction === 'asc'" class="text-xs text-primary">{{ t("shared.datatable.sortmenu.active.badge") }}</span>
|
|
39
|
+
</DropdownMenuItem>
|
|
40
|
+
<DropdownMenuItem class="gap-2" @select="pick(col.key, 'desc')">
|
|
41
|
+
<span class="flex-1 truncate">{{ col.label }} ↓</span>
|
|
42
|
+
<span v-if="sort.column === col.key && sort.direction === 'desc'" class="text-xs text-primary">{{ t("shared.datatable.sortmenu.active.badge") }}</span>
|
|
43
|
+
</DropdownMenuItem>
|
|
44
|
+
</template>
|
|
45
|
+
</DropdownMenuContent>
|
|
46
|
+
</DropdownMenu>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DataTableColumn, SortState } from './types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
columns: DataTableColumn[];
|
|
4
|
+
};
|
|
5
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
6
|
+
modelValue: SortState;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:modelValue": (value: SortState) => any;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
11
|
+
"onUpdate:modelValue"?: ((value: SortState) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List | Gallery (| …) switch for the DataTable toolbar.
|
|
3
|
+
*
|
|
4
|
+
* A gallery is a view mode of a table, not a different kind of page: the
|
|
5
|
+
* toolbar, the footer and the data pipeline stay put, only the body swaps
|
|
6
|
+
* for the `#view` slot.
|
|
7
|
+
*/
|
|
8
|
+
import type { DataTableViewMode } from './types.js';
|
|
9
|
+
type __VLS_Props = {
|
|
10
|
+
modes: DataTableViewMode[];
|
|
11
|
+
};
|
|
12
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
13
|
+
modelValue: string;
|
|
14
|
+
};
|
|
15
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
|
+
"update:modelValue": (value: string) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
18
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
modes: { type: Array, required: true }
|
|
4
|
+
});
|
|
5
|
+
const mode = defineModel({ type: String, ...{ required: true } });
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<div class="inline-flex shrink-0 overflow-hidden rounded-md border border-black/[0.06]">
|
|
10
|
+
<button
|
|
11
|
+
v-for="(m, i) in modes"
|
|
12
|
+
:key="m.value"
|
|
13
|
+
type="button"
|
|
14
|
+
class="inline-flex items-center gap-1.5 px-2.5 py-1 text-sm transition-colors"
|
|
15
|
+
:class="[
|
|
16
|
+
i > 0 ? 'border-l border-black/[0.06]' : '',
|
|
17
|
+
mode === m.value ? 'bg-primary/10 font-medium text-[#0B0522]' : 'text-muted-foreground hover:bg-muted/60'
|
|
18
|
+
]"
|
|
19
|
+
:aria-pressed="mode === m.value"
|
|
20
|
+
@click="mode = m.value"
|
|
21
|
+
>
|
|
22
|
+
<component :is="m.icon" v-if="m.icon" :size="13" />
|
|
23
|
+
{{ m.label }}
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List | Gallery (| …) switch for the DataTable toolbar.
|
|
3
|
+
*
|
|
4
|
+
* A gallery is a view mode of a table, not a different kind of page: the
|
|
5
|
+
* toolbar, the footer and the data pipeline stay put, only the body swaps
|
|
6
|
+
* for the `#view` slot.
|
|
7
|
+
*/
|
|
8
|
+
import type { DataTableViewMode } from './types.js';
|
|
9
|
+
type __VLS_Props = {
|
|
10
|
+
modes: DataTableViewMode[];
|
|
11
|
+
};
|
|
12
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
13
|
+
modelValue: string;
|
|
14
|
+
};
|
|
15
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
|
+
"update:modelValue": (value: string) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
18
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
export default _default;
|
|
@@ -56,6 +56,40 @@ export interface DataTableFilter<T = unknown> {
|
|
|
56
56
|
/** Label for the reset/"all" entry (default `All`). */
|
|
57
57
|
allLabel?: string;
|
|
58
58
|
}
|
|
59
|
+
/** A named snapshot of a list surface's filter + sort state. */
|
|
60
|
+
export interface DataTableSavedView<S = unknown> {
|
|
61
|
+
name: string;
|
|
62
|
+
snapshot: S;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Saved-view wiring for the DataTable toolbar. The table owns the control,
|
|
66
|
+
* the storage and the save/delete dialogs; the surface only says how to read
|
|
67
|
+
* and write its own filter state.
|
|
68
|
+
*/
|
|
69
|
+
export interface DataTableSavedViews<S = unknown> {
|
|
70
|
+
/** `localStorage` key — one bucket per list surface. */
|
|
71
|
+
storageKey: string;
|
|
72
|
+
/** Label of the reset entry (default `All`). */
|
|
73
|
+
allLabel?: string;
|
|
74
|
+
/** The surface's current filter + sort state. */
|
|
75
|
+
snapshot: () => S;
|
|
76
|
+
/** The unfiltered baseline the reset entry restores. */
|
|
77
|
+
defaultSnapshot: () => S;
|
|
78
|
+
/** Put a snapshot back onto the surface. */
|
|
79
|
+
apply: (snapshot: S) => void;
|
|
80
|
+
}
|
|
81
|
+
/** One entry of the toolbar's view-mode switch (List | Gallery | …). */
|
|
82
|
+
export interface DataTableViewMode {
|
|
83
|
+
/** `list` renders the table; any other value renders the `#view` slot. */
|
|
84
|
+
value: string;
|
|
85
|
+
label: string;
|
|
86
|
+
icon?: Component;
|
|
87
|
+
}
|
|
88
|
+
/** Collapsible left-rail affordance in the toolbar. */
|
|
89
|
+
export interface DataTableRail {
|
|
90
|
+
/** Button label (default `Filters`). */
|
|
91
|
+
label?: string;
|
|
92
|
+
}
|
|
59
93
|
export type SortDirection = 'asc' | 'desc' | null;
|
|
60
94
|
export interface SortState {
|
|
61
95
|
column: string | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StorageFolderNode } from '../../types/storage.js';
|
|
2
|
+
/**
|
|
3
|
+
* Recursive folder tree for the media browser sidebar. Emits `select` with a
|
|
4
|
+
* folder id (or null for the synthetic root). Self-renders for children.
|
|
5
|
+
*/
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
nodes: StorageFolderNode[];
|
|
8
|
+
currentId: string | null;
|
|
9
|
+
/** Internal: nesting depth for indentation. */
|
|
10
|
+
depth?: number;
|
|
11
|
+
};
|
|
12
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
13
|
+
select: (id: string | null) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
+
onSelect?: ((id: string | null) => any) | undefined;
|
|
16
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Folder as FolderIcon, FolderOpen, AltArrowRight } from "#solar-icons";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
nodes: { type: Array, required: true },
|
|
5
|
+
currentId: { type: [String, null], required: true },
|
|
6
|
+
depth: { type: Number, required: false }
|
|
7
|
+
});
|
|
8
|
+
const emit = defineEmits(["select"]);
|
|
9
|
+
const expanded = ref(/* @__PURE__ */ new Set());
|
|
10
|
+
function toggle(id) {
|
|
11
|
+
const next = new Set(expanded.value);
|
|
12
|
+
if (next.has(id)) next.delete(id);
|
|
13
|
+
else next.add(id);
|
|
14
|
+
expanded.value = next;
|
|
15
|
+
}
|
|
16
|
+
const depth = computed(() => props.depth ?? 0);
|
|
17
|
+
const t = useT();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div class="flex flex-col">
|
|
22
|
+
<div
|
|
23
|
+
v-for="node in nodes"
|
|
24
|
+
:key="node.id"
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
class="group flex items-center gap-1 rounded-md pr-2 py-1 text-sm transition-colors cursor-pointer"
|
|
28
|
+
:class="currentId === node.id ? 'bg-primary/10 text-[#0B0522] font-medium' : 'text-foreground/80 hover:bg-primary/5'"
|
|
29
|
+
:style="{ paddingLeft: `${depth * 12 + 4}px` }"
|
|
30
|
+
@click="emit('select', node.id)"
|
|
31
|
+
>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
class="shrink-0 rounded p-0.5 text-foreground/40 hover:text-foreground/80"
|
|
35
|
+
:class="{ invisible: node.children.length === 0 }"
|
|
36
|
+
@click.stop="toggle(node.id)"
|
|
37
|
+
>
|
|
38
|
+
<AltArrowRight
|
|
39
|
+
:size="12"
|
|
40
|
+
class="transition-transform"
|
|
41
|
+
:class="{ 'rotate-90': expanded.has(node.id) }"
|
|
42
|
+
/>
|
|
43
|
+
</button>
|
|
44
|
+
<component
|
|
45
|
+
:is="currentId === node.id ? FolderOpen : FolderIcon"
|
|
46
|
+
:size="14"
|
|
47
|
+
class="shrink-0 text-foreground/50"
|
|
48
|
+
/>
|
|
49
|
+
<span class="flex-1 truncate">{{ node.name }}</span>
|
|
50
|
+
<span
|
|
51
|
+
v-if="node.is_system"
|
|
52
|
+
class="rounded bg-muted px-1 text-[10px] font-medium text-muted-foreground"
|
|
53
|
+
>{{ t("shared.media.foldertree.badge.system") }}</span>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<MediaFolderTree
|
|
57
|
+
v-if="node.children.length && expanded.has(node.id)"
|
|
58
|
+
:nodes="node.children"
|
|
59
|
+
:current-id="currentId"
|
|
60
|
+
:depth="depth + 1"
|
|
61
|
+
@select="(id) => emit('select', id)"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StorageFolderNode } from '../../types/storage.js';
|
|
2
|
+
/**
|
|
3
|
+
* Recursive folder tree for the media browser sidebar. Emits `select` with a
|
|
4
|
+
* folder id (or null for the synthetic root). Self-renders for children.
|
|
5
|
+
*/
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
nodes: StorageFolderNode[];
|
|
8
|
+
currentId: string | null;
|
|
9
|
+
/** Internal: nesting depth for indentation. */
|
|
10
|
+
depth?: number;
|
|
11
|
+
};
|
|
12
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
13
|
+
select: (id: string | null) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
+
onSelect?: ((id: string | null) => any) | undefined;
|
|
16
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
+
export default _default;
|
|
@@ -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,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>
|