@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,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A card sized for the `RecordLayout` rail (PE-250).
|
|
3
|
+
*
|
|
4
|
+
* Same glass shell as the page cards, but on the rail's budget: `px-4 py-3`
|
|
5
|
+
* instead of `px-6 py-4`, a `text-xs` section label instead of a headline, and
|
|
6
|
+
* an optional right-aligned `#actions` (usually the link that jumps to the tab
|
|
7
|
+
* that owns the full thing). Use it so every rail across the studio stacks the
|
|
8
|
+
* same way instead of each page re-rolling its own `<section>`.
|
|
9
|
+
*/
|
|
10
|
+
type __VLS_Props = {
|
|
11
|
+
title?: string;
|
|
12
|
+
};
|
|
13
|
+
declare var __VLS_1: {}, __VLS_3: {};
|
|
14
|
+
type __VLS_Slots = {} & {
|
|
15
|
+
actions?: (props: typeof __VLS_1) => any;
|
|
16
|
+
} & {
|
|
17
|
+
default?: (props: typeof __VLS_3) => any;
|
|
18
|
+
};
|
|
19
|
+
declare const __VLS_component: 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>;
|
|
20
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
21
|
+
export default _default;
|
|
22
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
23
|
+
new (): {
|
|
24
|
+
$slots: S;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
title: { type: String, required: false }
|
|
4
|
+
});
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<section class="overflow-hidden rounded-lg border border-gray-400/10 bg-white/60 outline-1 outline-white/30 backdrop-blur-sm">
|
|
9
|
+
<div
|
|
10
|
+
v-if="title || $slots.actions"
|
|
11
|
+
class="flex items-center justify-between gap-2 border-b border-black/[0.06] px-4 py-2.5"
|
|
12
|
+
>
|
|
13
|
+
<h3 class="truncate text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
14
|
+
{{ title }}
|
|
15
|
+
</h3>
|
|
16
|
+
<slot name="actions" />
|
|
17
|
+
</div>
|
|
18
|
+
<slot />
|
|
19
|
+
</section>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A card sized for the `RecordLayout` rail (PE-250).
|
|
3
|
+
*
|
|
4
|
+
* Same glass shell as the page cards, but on the rail's budget: `px-4 py-3`
|
|
5
|
+
* instead of `px-6 py-4`, a `text-xs` section label instead of a headline, and
|
|
6
|
+
* an optional right-aligned `#actions` (usually the link that jumps to the tab
|
|
7
|
+
* that owns the full thing). Use it so every rail across the studio stacks the
|
|
8
|
+
* same way instead of each page re-rolling its own `<section>`.
|
|
9
|
+
*/
|
|
10
|
+
type __VLS_Props = {
|
|
11
|
+
title?: string;
|
|
12
|
+
};
|
|
13
|
+
declare var __VLS_1: {}, __VLS_3: {};
|
|
14
|
+
type __VLS_Slots = {} & {
|
|
15
|
+
actions?: (props: typeof __VLS_1) => any;
|
|
16
|
+
} & {
|
|
17
|
+
default?: (props: typeof __VLS_3) => any;
|
|
18
|
+
};
|
|
19
|
+
declare const __VLS_component: 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>;
|
|
20
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
21
|
+
export default _default;
|
|
22
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
23
|
+
new (): {
|
|
24
|
+
$slots: S;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* studio sidebar (Settings, Integrations, Commerce, Apps, …) renders
|
|
6
6
|
* through this so spacing, active state, badges and "soon" tags stay
|
|
7
7
|
* identical. Pass `sections`; use `#notice` for loading/empty hints.
|
|
8
|
+
*
|
|
9
|
+
* Items may declare `children`. Those are **tabs of the page the item opens**,
|
|
10
|
+
* not sub-navigation: every item is exactly one row, and it stays highlighted
|
|
11
|
+
* while the route is on any of its tabs. A surface that should stand on its
|
|
12
|
+
* own in the sidebar is declared as its own item.
|
|
8
13
|
*/
|
|
9
14
|
import type { Component } from 'vue';
|
|
10
15
|
import type { SidebarNavSection } from '../../types/nav.js';
|
|
@@ -3,17 +3,14 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script setup>
|
|
6
|
+
import { sidebarItemKey } from "../../lib/sidebarNav";
|
|
7
|
+
import SidebarNavLink from "./SidebarNavLink.vue";
|
|
6
8
|
defineProps({
|
|
7
9
|
title: { type: String, required: true },
|
|
8
10
|
icon: { type: null, required: false },
|
|
9
11
|
sections: { type: Array, required: true },
|
|
10
12
|
minimal: { type: Boolean, required: false }
|
|
11
13
|
});
|
|
12
|
-
const route = useRoute();
|
|
13
|
-
function isActive(item) {
|
|
14
|
-
if (item.exact) return route.path === item.to;
|
|
15
|
-
return route.path === item.to || route.path.startsWith(`${item.to}/`);
|
|
16
|
-
}
|
|
17
14
|
</script>
|
|
18
15
|
|
|
19
16
|
<template>
|
|
@@ -37,28 +34,11 @@ function isActive(item) {
|
|
|
37
34
|
</div>
|
|
38
35
|
</div>
|
|
39
36
|
<nav class="flex flex-col gap-0.5 px-2">
|
|
40
|
-
<
|
|
37
|
+
<SidebarNavLink
|
|
41
38
|
v-for="item in section.items"
|
|
42
|
-
:key="item
|
|
43
|
-
:
|
|
44
|
-
|
|
45
|
-
:class="[
|
|
46
|
-
isActive(item) ? 'bg-primary/10 text-[#0B0522] font-medium' : 'text-foreground/80 hover:bg-primary/5',
|
|
47
|
-
item.preview ? 'opacity-70' : ''
|
|
48
|
-
]"
|
|
49
|
-
>
|
|
50
|
-
<component :is="item.icon" v-if="item.icon" :size="14" :stroke-width="2" class="shrink-0 text-foreground/60 group-hover:text-primary" />
|
|
51
|
-
<span class="flex-1 truncate flex items-center gap-1.5">
|
|
52
|
-
{{ item.label }}
|
|
53
|
-
<span
|
|
54
|
-
v-if="item.preview"
|
|
55
|
-
class="rounded bg-muted px-1 text-mono text-muted-foreground"
|
|
56
|
-
title="Coming soon"
|
|
57
|
-
>
|
|
58
|
-
soon
|
|
59
|
-
</span>
|
|
60
|
-
</span>
|
|
61
|
-
</NuxtLink>
|
|
39
|
+
:key="sidebarItemKey(item)"
|
|
40
|
+
:item="item"
|
|
41
|
+
/>
|
|
62
42
|
</nav>
|
|
63
43
|
</template>
|
|
64
44
|
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* studio sidebar (Settings, Integrations, Commerce, Apps, …) renders
|
|
6
6
|
* through this so spacing, active state, badges and "soon" tags stay
|
|
7
7
|
* identical. Pass `sections`; use `#notice` for loading/empty hints.
|
|
8
|
+
*
|
|
9
|
+
* Items may declare `children`. Those are **tabs of the page the item opens**,
|
|
10
|
+
* not sub-navigation: every item is exactly one row, and it stays highlighted
|
|
11
|
+
* while the route is on any of its tabs. A surface that should stand on its
|
|
12
|
+
* own in the sidebar is declared as its own item.
|
|
8
13
|
*/
|
|
9
14
|
import type { Component } from 'vue';
|
|
10
15
|
import type { SidebarNavSection } from '../../types/nav.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One row of a `SidebarNav` section — always exactly one link, never a group.
|
|
3
|
+
*
|
|
4
|
+
* An item's `children` are the **tabs of the page this row opens**, not rows
|
|
5
|
+
* of their own (see `lib/sidebarNav.ts`), so there is nothing to expand here:
|
|
6
|
+
* the row links to the item's route, or — for a pure container that nominates
|
|
7
|
+
* no landing page — to its first tab, and stays highlighted while the route is
|
|
8
|
+
* on any of them.
|
|
9
|
+
*/
|
|
10
|
+
import type { SidebarNavItem } from '../../types/nav.js';
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
item: SidebarNavItem;
|
|
13
|
+
};
|
|
14
|
+
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>;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { sidebarItemIsCurrent, sidebarItemTarget } from "../../lib/sidebarNav";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
item: { type: Object, required: true }
|
|
5
|
+
});
|
|
6
|
+
const t = useT();
|
|
7
|
+
const route = useRoute();
|
|
8
|
+
const target = computed(() => sidebarItemTarget(props.item));
|
|
9
|
+
const current = computed(() => sidebarItemIsCurrent(props.item, route.path));
|
|
10
|
+
const rowClass = computed(() => [
|
|
11
|
+
"group flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors hover:text-primary",
|
|
12
|
+
current.value ? "bg-primary/10 text-[#0B0522] font-medium" : "text-foreground/80 hover:bg-primary/5",
|
|
13
|
+
props.item.preview ? "opacity-70" : ""
|
|
14
|
+
]);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<NuxtLink v-if="target" :to="target" :class="rowClass">
|
|
19
|
+
<component :is="item.icon" v-if="item.icon" :size="14" :stroke-width="2" class="shrink-0 text-foreground/60 group-hover:text-primary" />
|
|
20
|
+
<span class="min-w-0 flex-1 truncate flex items-center gap-1.5">
|
|
21
|
+
{{ item.label }}
|
|
22
|
+
<span
|
|
23
|
+
v-if="item.preview"
|
|
24
|
+
class="rounded bg-muted px-1 text-mono text-muted-foreground"
|
|
25
|
+
:title="t('shared.nav.badge.comingsoon.tooltip')"
|
|
26
|
+
>
|
|
27
|
+
{{ t("shared.nav.badge.comingsoon.label") }}
|
|
28
|
+
</span>
|
|
29
|
+
</span>
|
|
30
|
+
<!--
|
|
31
|
+
What is waiting behind the entry. A count sits at the end of the row so
|
|
32
|
+
the labels stay left-aligned and scannable; a dot only says "something",
|
|
33
|
+
which is the honest rendering when the source cannot be counted.
|
|
34
|
+
Resolved upstream — an item with nothing pending carries no badge at all
|
|
35
|
+
rather than a zero, because a zero reads as a queue worth opening.
|
|
36
|
+
-->
|
|
37
|
+
<span
|
|
38
|
+
v-if="item.badge === 'dot'"
|
|
39
|
+
class="size-1.5 shrink-0 rounded-full bg-primary"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
/>
|
|
42
|
+
<span
|
|
43
|
+
v-else-if="item.badge !== null && item.badge !== void 0"
|
|
44
|
+
class="shrink-0 rounded-full bg-muted px-1.5 text-[11px] font-medium tabular-nums text-muted-foreground"
|
|
45
|
+
>
|
|
46
|
+
{{ item.badge }}
|
|
47
|
+
</span>
|
|
48
|
+
</NuxtLink>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One row of a `SidebarNav` section — always exactly one link, never a group.
|
|
3
|
+
*
|
|
4
|
+
* An item's `children` are the **tabs of the page this row opens**, not rows
|
|
5
|
+
* of their own (see `lib/sidebarNav.ts`), so there is nothing to expand here:
|
|
6
|
+
* the row links to the item's route, or — for a pure container that nominates
|
|
7
|
+
* no landing page — to its first tab, and stays highlighted while the route is
|
|
8
|
+
* on any of them.
|
|
9
|
+
*/
|
|
10
|
+
import type { SidebarNavItem } from '../../types/nav.js';
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
item: SidebarNavItem;
|
|
13
|
+
};
|
|
14
|
+
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>;
|
|
15
|
+
export default _default;
|
|
@@ -7,6 +7,7 @@ const props = defineProps({
|
|
|
7
7
|
busy: { type: Boolean, required: false, default: false }
|
|
8
8
|
});
|
|
9
9
|
const emit = defineEmits(["update:modelValue", "finish"]);
|
|
10
|
+
const t = useT();
|
|
10
11
|
const index = computed({
|
|
11
12
|
get: () => props.modelValue,
|
|
12
13
|
set: (v) => emit("update:modelValue", v)
|
|
@@ -40,7 +41,7 @@ function goTo(i) {
|
|
|
40
41
|
@click="goTo(i)"
|
|
41
42
|
>
|
|
42
43
|
<div class="h-1 rounded-full transition-colors" :class="i <= index ? 'bg-primary' : 'bg-border'" />
|
|
43
|
-
<div class="mt-2 text-xs font-medium" :class="i <= index ? 'text-primary' : 'text-muted-foreground'">
|
|
44
|
+
<div class="mt-2 text-xs font-medium" :class="i <= index ? 'text-primary' : 'text-muted-foreground'">{{ t("shared.wizard.step.label", { number: i + 1 }) }}</div>
|
|
44
45
|
<div class="text-sm" :class="i <= index ? 'text-[#0B0522]' : 'text-muted-foreground'">{{ s.title }}</div>
|
|
45
46
|
</div>
|
|
46
47
|
</div>
|
|
@@ -53,9 +54,11 @@ function goTo(i) {
|
|
|
53
54
|
<!-- Custom footer (e.g. multiple finish actions on a review step). Falls
|
|
54
55
|
back to the standard Back / Next·Finish pair. -->
|
|
55
56
|
<slot name="footer" :back="back" :next="next" :is-first="isFirst" :is-last="isLast" :can-advance="canAdvance" :busy="busy" :index="index">
|
|
56
|
-
<Button variant="ghost" :disabled="isFirst || busy" @click="back">
|
|
57
|
+
<Button variant="ghost" :disabled="isFirst || busy" @click="back">
|
|
58
|
+
{{ t("shared.wizard.action.back") }}
|
|
59
|
+
</Button>
|
|
57
60
|
<Button :disabled="!canAdvance || busy" class="gap-1" @click="next">
|
|
58
|
-
{{ isLast ? "
|
|
61
|
+
{{ isLast ? t("shared.wizard.action.finish") : t("shared.wizard.action.next") }}
|
|
59
62
|
</Button>
|
|
60
63
|
</slot>
|
|
61
64
|
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open/closed state for a list surface's left rail, remembered per surface.
|
|
3
|
+
*
|
|
4
|
+
* The rail is a filter affordance, not content: an operator who collapsed it
|
|
5
|
+
* to give the table the full width expects it to stay collapsed on the next
|
|
6
|
+
* visit. Reads happen on mount (SSR-safe) and every change is written back;
|
|
7
|
+
* a browser without usable storage simply keeps the state for the session.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useListRail(key: string, initial?: boolean): any;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function useListRail(key, initial = true) {
|
|
2
|
+
const storageKey = `commerce.listRail.${key}`;
|
|
3
|
+
const open = ref(initial);
|
|
4
|
+
onMounted(() => {
|
|
5
|
+
try {
|
|
6
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
7
|
+
if (raw != null) open.value = raw === "1";
|
|
8
|
+
} catch {
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
watch(open, (v) => {
|
|
12
|
+
try {
|
|
13
|
+
if (typeof window === "undefined") return;
|
|
14
|
+
window.localStorage.setItem(storageKey, v ? "1" : "0");
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return open;
|
|
19
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
import type { StorageAsset, StorageFolder, StorageFolderNode, UploadProgress } from '../types/storage.js';
|
|
3
|
+
/**
|
|
4
|
+
* Media library provider seam — the storage client behind the shared media
|
|
5
|
+
* components (`MediaUploader`, `MediaPickerDialog`, `MediaFolderTree`).
|
|
6
|
+
*
|
|
7
|
+
* Same shape as the `usePlatformAuth` / `usePlatformTenant` contract: the
|
|
8
|
+
* components never import a host's storage client, they call this. The
|
|
9
|
+
* standalone default below is inert (no assets, uploads report as failed), so a
|
|
10
|
+
* module host that hasn't wired Storage still renders the dialogs instead of
|
|
11
|
+
* crashing. The full cockpit overrides it at `app/composables/useMediaLibrary.ts`
|
|
12
|
+
* — an app-level auto-import beats this module-registered one (priority -1).
|
|
13
|
+
*/
|
|
14
|
+
export type { StorageAsset, StorageFolder, StorageFolderNode, UploadProgress };
|
|
15
|
+
export interface MediaLibraryClient {
|
|
16
|
+
/** Assets in the current view. */
|
|
17
|
+
assets: Ref<StorageAsset[]>;
|
|
18
|
+
/** Folders nested by `parent_id`, each level sorted by name. */
|
|
19
|
+
folderTree: Ref<StorageFolderNode[]>;
|
|
20
|
+
currentFolderId: Ref<string | null>;
|
|
21
|
+
/** 'all' = every asset; 'unfiled' = folder-less; 'folder' = `currentFolderId`. */
|
|
22
|
+
currentView: Ref<'all' | 'unfiled' | 'folder'>;
|
|
23
|
+
currentFolder: Ref<StorageFolder | null>;
|
|
24
|
+
loadingAssets: Ref<boolean>;
|
|
25
|
+
hasMore: Ref<boolean>;
|
|
26
|
+
/** Load folders + the current view's assets. */
|
|
27
|
+
init: () => Promise<void>;
|
|
28
|
+
openFolder: (folderId: string) => Promise<void>;
|
|
29
|
+
showAllMedia: () => Promise<void>;
|
|
30
|
+
loadMore: () => Promise<void>;
|
|
31
|
+
/** Upload a batch into `folderId`, reporting per-file progress. */
|
|
32
|
+
uploadMany: (files: File[], folderId?: string | null, onItem?: (index: number, progress: UploadProgress) => void) => Promise<{
|
|
33
|
+
created: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
}>;
|
|
36
|
+
/** Displayable thumbnail URL, or null when the kind has no preview. */
|
|
37
|
+
thumbnailUrl: (asset: StorageAsset, size?: number) => string | null;
|
|
38
|
+
/** True while the asset is still being scanned/processed. */
|
|
39
|
+
isProcessing: (asset: StorageAsset) => boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare function useMediaLibrary(): MediaLibraryClient;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
export function useMediaLibrary() {
|
|
3
|
+
const assets = ref([]);
|
|
4
|
+
const folderTree = ref([]);
|
|
5
|
+
const currentFolderId = ref(null);
|
|
6
|
+
const currentView = ref("all");
|
|
7
|
+
const currentFolder = computed(() => null);
|
|
8
|
+
const loadingAssets = ref(false);
|
|
9
|
+
const hasMore = ref(false);
|
|
10
|
+
const noop = async () => {
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
assets,
|
|
14
|
+
folderTree,
|
|
15
|
+
currentFolderId,
|
|
16
|
+
currentView,
|
|
17
|
+
currentFolder,
|
|
18
|
+
loadingAssets,
|
|
19
|
+
hasMore,
|
|
20
|
+
init: noop,
|
|
21
|
+
openFolder: noop,
|
|
22
|
+
showAllMedia: noop,
|
|
23
|
+
loadMore: noop,
|
|
24
|
+
async uploadMany(files, _folderId, onItem) {
|
|
25
|
+
files.forEach((file, i) => onItem?.(i, {
|
|
26
|
+
name: file.name,
|
|
27
|
+
pct: 0,
|
|
28
|
+
status: "failed",
|
|
29
|
+
error: useT()("shared.media.uploader.error.nostorage")
|
|
30
|
+
}));
|
|
31
|
+
return { created: 0, failed: files.length };
|
|
32
|
+
},
|
|
33
|
+
thumbnailUrl: () => null,
|
|
34
|
+
isProcessing: () => false
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
1
2
|
/**
|
|
2
3
|
* Platform auth provider — the seam that lets a studio surface run either
|
|
3
4
|
* inside the full cockpit or standalone.
|
|
@@ -13,5 +14,22 @@
|
|
|
13
14
|
*/
|
|
14
15
|
export interface PlatformAuth {
|
|
15
16
|
getToken: () => Promise<string | null>;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a token is available NOW, as a reactive flag. OPTIONAL, because the
|
|
19
|
+
* standalone default below has nothing to report — a value in `localStorage` is
|
|
20
|
+
* either there or not, with no moment at which it becomes there.
|
|
21
|
+
*
|
|
22
|
+
* It exists for the hosts where `getToken()` legitimately answers null for a
|
|
23
|
+
* while and then starts working: the cockpit's override refreshes through
|
|
24
|
+
* `useConsoleAuth`, which backs off for 15s after a failed refresh and returns
|
|
25
|
+
* null for the duration. A consumer that only reacts to something ELSE changing
|
|
26
|
+
* — `usePlatformTalkback` watched the tenant slug — never retries and stays
|
|
27
|
+
* dead for the life of the tab.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately a flag rather than the token itself: nothing outside the auth
|
|
30
|
+
* seam should hold a token it did not just ask for, and a stale one in a `ref`
|
|
31
|
+
* is exactly how that happens.
|
|
32
|
+
*/
|
|
33
|
+
ready?: Readonly<Ref<boolean>>;
|
|
16
34
|
}
|
|
17
35
|
export declare function usePlatformAuth(): PlatformAuth;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ShallowRef } from 'vue';
|
|
2
|
+
import type { Talkback } from '@revenexx/talkback-js';
|
|
3
|
+
/**
|
|
4
|
+
* Platform realtime provider — the third seam beside `usePlatformAuth` and
|
|
5
|
+
* `usePlatformTenant`, and it composes both of them.
|
|
6
|
+
*
|
|
7
|
+
* A studio module never builds a Talkback client itself: it calls
|
|
8
|
+
* `useTalkbackTopic()` / `useTalkbackResource()` and this decides whether there
|
|
9
|
+
* is a connection behind them. One client for the whole application — a second
|
|
10
|
+
* `createTalkback` would open a second WebSocket and mint its own connection
|
|
11
|
+
* token.
|
|
12
|
+
*
|
|
13
|
+
* OFF IS A FIRST-CLASS STATE. An empty `talkbackHost` means no client is built,
|
|
14
|
+
* neither `@revenexx/talkback-js` nor `centrifuge` is loaded, and the channel
|
|
15
|
+
* composables become no-ops — so a call site needs no guard and a host that has
|
|
16
|
+
* no realtime stack is not half-connected. That is also the production gate: the
|
|
17
|
+
* edge has not been shown to pass a WebSocket upgrade yet (PE-382 T0.2).
|
|
18
|
+
*/
|
|
19
|
+
export interface PlatformTalkback {
|
|
20
|
+
/**
|
|
21
|
+
* The client, or `null` while it is still loading or realtime is off.
|
|
22
|
+
* Reactive: the client arrives after a dynamic import, so a component that
|
|
23
|
+
* rendered first has to be able to pick it up.
|
|
24
|
+
*/
|
|
25
|
+
talkback: ShallowRef<Talkback | null>;
|
|
26
|
+
/** Whether this host has realtime configured at all. */
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function usePlatformTalkback(): PlatformTalkback;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { watch } from "vue";
|
|
2
|
+
import { createPlatformTalkback, setTalkback, talkbackRef } from "../lib/talkback.js";
|
|
3
|
+
function withAppBase(baseURL, path) {
|
|
4
|
+
if (/^https?:\/\//.test(path)) return path;
|
|
5
|
+
const base = (baseURL || "/").replace(/\/+$/, "");
|
|
6
|
+
return `${base}/${path.replace(/^\/+/, "")}`;
|
|
7
|
+
}
|
|
8
|
+
let started = false;
|
|
9
|
+
export function usePlatformTalkback() {
|
|
10
|
+
const config = useRuntimeConfig();
|
|
11
|
+
const host = String(config.public.talkbackHost || "");
|
|
12
|
+
const enabled = Boolean(host);
|
|
13
|
+
if (enabled && import.meta.client && !started) {
|
|
14
|
+
started = true;
|
|
15
|
+
const { getToken, ready: authReady } = usePlatformAuth();
|
|
16
|
+
const { slug, load } = usePlatformTenant();
|
|
17
|
+
const baseURL = String(config.app?.baseURL || "/");
|
|
18
|
+
const facade = host.replace(/\/+$/, "");
|
|
19
|
+
const tokenEndpoint = withAppBase(baseURL, String(config.public.talkbackTokenEndpoint || `${facade}/v1/tokens`));
|
|
20
|
+
const subscriptionTokenEndpoint = withAppBase(baseURL, String(config.public.talkbackSubscriptionTokenEndpoint || `${facade}/v1/subscription-tokens`));
|
|
21
|
+
const start = (async () => {
|
|
22
|
+
const tb = await createPlatformTalkback({
|
|
23
|
+
host,
|
|
24
|
+
tokenEndpoint,
|
|
25
|
+
subscriptionTokenEndpoint,
|
|
26
|
+
tenant: () => slug.value ?? "",
|
|
27
|
+
getToken
|
|
28
|
+
});
|
|
29
|
+
setTalkback(tb);
|
|
30
|
+
let applied = null;
|
|
31
|
+
async function sync() {
|
|
32
|
+
if (!slug.value) {
|
|
33
|
+
try {
|
|
34
|
+
await load();
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const token = await getToken();
|
|
39
|
+
const next = slug.value && token ? slug.value : null;
|
|
40
|
+
if (next === applied) return;
|
|
41
|
+
tb.disconnect();
|
|
42
|
+
applied = next;
|
|
43
|
+
if (next) tb.connect();
|
|
44
|
+
}
|
|
45
|
+
const inputs = authReady ? [slug, authReady] : [slug];
|
|
46
|
+
watch(inputs, () => void sync(), { immediate: true });
|
|
47
|
+
})();
|
|
48
|
+
void start.catch((error) => {
|
|
49
|
+
if (!talkbackRef().value) started = false;
|
|
50
|
+
console.warn("[studio-shared] talkback failed to start", error);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { talkback: talkbackRef(), enabled };
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue';
|
|
2
|
+
import type { Translate } from '../lib/i18nBridge.js';
|
|
3
|
+
export declare function useT(): Translate;
|
|
4
|
+
/**
|
|
5
|
+
* The host's current UI locale (`'en'` when the host has no i18n).
|
|
6
|
+
*
|
|
7
|
+
* This is the INTERFACE language, not a content locale: it says which language
|
|
8
|
+
* the chrome is in, never which locale of a `labels` jsonb to show. For the
|
|
9
|
+
* latter see `useTenantLocales()` in `@revenexx/studio-commerce`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useUiLocale(): ComputedRef<string>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue';
|
|
2
|
+
import type { EnvelopeListener, RawListener, ResyncContext } from '@revenexx/talkback-js';
|
|
3
|
+
import type { MaybeRefOrGetter } from '@revenexx/talkback-js/vue';
|
|
4
|
+
/**
|
|
5
|
+
* The five channel composables, re-exported through this package so a studio
|
|
6
|
+
* module can subscribe with a bare name and carry no Talkback dependency of its
|
|
7
|
+
* own — the same contract `usePlatformAuth` / `usePlatformTenant` already have.
|
|
8
|
+
*
|
|
9
|
+
* THREE THINGS THESE WRAPPERS ADD, and each is a bug without them:
|
|
10
|
+
*
|
|
11
|
+
* 1. **The client is passed explicitly, never injected.** talkback-js's
|
|
12
|
+
* `useTalkback()` reads a module-PRIVATE `Symbol`, so two copies of the
|
|
13
|
+
* package are two different keys: it throws inside a studio module while the
|
|
14
|
+
* host looks healthy. Its `talkback` option exists for exactly this, and
|
|
15
|
+
* using it means no consumer has to know a plugin ran in the right order.
|
|
16
|
+
*
|
|
17
|
+
* 2. **They tolerate a client that does not exist yet.** The client arrives
|
|
18
|
+
* after a dynamic import, so any component that rendered first would
|
|
19
|
+
* otherwise subscribe to nothing and never retry. The delegate runs in a
|
|
20
|
+
* child `effectScope` that is created when the client appears and stopped
|
|
21
|
+
* with the caller's scope — which is also what gives the delegate's own
|
|
22
|
+
* `onScopeDispose(release)` a scope to bind to.
|
|
23
|
+
*
|
|
24
|
+
* 3. **`channel` is actually reactive.** Up to and including
|
|
25
|
+
* `@revenexx/talkback-js@0.1.3`, `ChannelSubscription.channel` is
|
|
26
|
+
* `{ value: null }` cast to a `Ref` — `ref` is never imported, so `isRef()`
|
|
27
|
+
* is false and a template binding never updates. Fixed upstream in
|
|
28
|
+
* talkback-js#11 but unreleased; this wrapper can be simplified to a
|
|
29
|
+
* pass-through once that ships.
|
|
30
|
+
*
|
|
31
|
+
* With realtime off (`talkbackHost` empty) every one of these is a no-op that
|
|
32
|
+
* reports a null channel, so a call site needs no `if (enabled)`.
|
|
33
|
+
*/
|
|
34
|
+
/** Re-exported so a consumer types a ref-able argument without importing talkback-js. */
|
|
35
|
+
export type TalkbackMaybeRef<T> = MaybeRefOrGetter<T>;
|
|
36
|
+
export interface TalkbackChannelOptions {
|
|
37
|
+
/** One action, or several. Omit to receive every action on the channel. */
|
|
38
|
+
on?: string | readonly string[];
|
|
39
|
+
/** Called for each matching envelope. Events are refetch signals, not state. */
|
|
40
|
+
handler?: EnvelopeListener;
|
|
41
|
+
/** Every publication, envelope or not — how a `stream:` channel is read. */
|
|
42
|
+
raw?: RawListener;
|
|
43
|
+
/** The recovery buffer could not close the gap: refetch over HTTP. */
|
|
44
|
+
onResync?: (ctx: ResyncContext) => void;
|
|
45
|
+
/** Skip subscribing, e.g. while a route parameter is still empty. */
|
|
46
|
+
enabled?: TalkbackMaybeRef<boolean>;
|
|
47
|
+
}
|
|
48
|
+
export interface TalkbackChannel {
|
|
49
|
+
/** The currently subscribed channel, or null when nothing is subscribed. */
|
|
50
|
+
channel: ComputedRef<string | null>;
|
|
51
|
+
/** Releases early. Called automatically when the calling scope is disposed. */
|
|
52
|
+
stop: () => void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The ACTION channel for a topic — a grid watching every instance of a kind.
|
|
56
|
+
*
|
|
57
|
+
* THE TOPIC INCLUDES THE ACTION, and one action is one channel (Centrifugo has
|
|
58
|
+
* no wildcards), so a grid watching three actions takes three calls:
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* for (const action of ['started', 'finished', 'failed'] as const) {
|
|
62
|
+
* useTalkbackTopic(`revenexx.integrations.run.${action}`, { handler: () => load(true) })
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function useTalkbackTopic(topic: TalkbackMaybeRef<string>, options?: TalkbackChannelOptions): TalkbackChannel;
|
|
67
|
+
/**
|
|
68
|
+
* The RESOURCE channel — one detail view, every action on it. The id may be a
|
|
69
|
+
* ref or a getter, and the subscription follows it, releasing the old channel
|
|
70
|
+
* before taking the new one.
|
|
71
|
+
*/
|
|
72
|
+
export declare function useTalkbackResource(topic: TalkbackMaybeRef<string>, topicId: TalkbackMaybeRef<string | null | undefined>, options?: TalkbackChannelOptions): TalkbackChannel;
|
|
73
|
+
/** The signed-in user's own channel — notifications and toasts. */
|
|
74
|
+
export declare function useTalkbackUser(options?: TalkbackChannelOptions): TalkbackChannel;
|
|
75
|
+
/** A `stream:` channel. `onResync` fires on EVERY subscribe here — it keeps no history. */
|
|
76
|
+
export declare function useTalkbackStream(streamId: TalkbackMaybeRef<string | null | undefined>, options?: TalkbackChannelOptions): TalkbackChannel;
|
|
77
|
+
/** Who else is looking at this resource. */
|
|
78
|
+
export declare function useTalkbackPresence(vendor: TalkbackMaybeRef<string>, app: TalkbackMaybeRef<string>, entity: TalkbackMaybeRef<string>, id?: TalkbackMaybeRef<string | null | undefined>, options?: TalkbackChannelOptions): TalkbackChannel;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { computed, effectScope, onScopeDispose, ref, shallowRef, watch } from "vue";
|
|
2
|
+
import { talkbackRef } from "../lib/talkback.js";
|
|
3
|
+
function useDelegate(subscribe) {
|
|
4
|
+
const client = talkbackRef();
|
|
5
|
+
const name = ref(null);
|
|
6
|
+
const scope = shallowRef(null);
|
|
7
|
+
function stop() {
|
|
8
|
+
scope.value?.stop();
|
|
9
|
+
scope.value = null;
|
|
10
|
+
name.value = null;
|
|
11
|
+
}
|
|
12
|
+
const unwatch = watch(client, (tb) => {
|
|
13
|
+
stop();
|
|
14
|
+
if (!tb) return;
|
|
15
|
+
const child = effectScope();
|
|
16
|
+
scope.value = child;
|
|
17
|
+
void (async () => {
|
|
18
|
+
const vue = await import("@revenexx/talkback-js/vue");
|
|
19
|
+
if (scope.value !== child) return;
|
|
20
|
+
child.run(() => {
|
|
21
|
+
let sub;
|
|
22
|
+
try {
|
|
23
|
+
sub = subscribe(vue, tb);
|
|
24
|
+
} catch {
|
|
25
|
+
name.value = null;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
watch(() => sub.channel.value, (v) => name.value = v, { immediate: true, flush: "sync" });
|
|
29
|
+
});
|
|
30
|
+
})();
|
|
31
|
+
}, { immediate: true });
|
|
32
|
+
onScopeDispose(() => {
|
|
33
|
+
unwatch();
|
|
34
|
+
stop();
|
|
35
|
+
});
|
|
36
|
+
return { channel: computed(() => name.value), stop };
|
|
37
|
+
}
|
|
38
|
+
export function useTalkbackTopic(topic, options = {}) {
|
|
39
|
+
return useDelegate((vue, tb) => vue.useTalkbackTopic(topic, { ...options, talkback: tb }));
|
|
40
|
+
}
|
|
41
|
+
export function useTalkbackResource(topic, topicId, options = {}) {
|
|
42
|
+
return useDelegate((vue, tb) => vue.useTalkbackResource(topic, topicId, { ...options, talkback: tb }));
|
|
43
|
+
}
|
|
44
|
+
export function useTalkbackUser(options = {}) {
|
|
45
|
+
return useDelegate((vue, tb) => vue.useTalkbackUser({ ...options, talkback: tb }));
|
|
46
|
+
}
|
|
47
|
+
export function useTalkbackStream(streamId, options = {}) {
|
|
48
|
+
return useDelegate((vue, tb) => vue.useTalkbackStream(streamId, { ...options, talkback: tb }));
|
|
49
|
+
}
|
|
50
|
+
export function useTalkbackPresence(vendor, app, entity, id = () => null, options = {}) {
|
|
51
|
+
return useDelegate((vue, tb) => vue.useTalkbackPresence(vendor, app, entity, id, { ...options, talkback: tb }));
|
|
52
|
+
}
|