@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.
- package/README.md +22 -12
- package/dist/module.d.mts +15 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +58 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +12 -0
- package/{components → dist/runtime/components}/DataTable/ColumnsMenu.vue +13 -19
- package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +12 -0
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +87 -0
- package/dist/runtime/components/DataTable/DataTable.vue +510 -0
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +87 -0
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +21 -0
- package/dist/runtime/components/DataTable/FilterMenu.vue +46 -0
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +21 -0
- 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 +104 -0
- package/dist/runtime/components/DataTable/types.js +0 -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.d.vue.ts +30 -0
- package/{components → dist/runtime/components}/Shared/BuildLogsFlyout.vue +37 -54
- package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +30 -0
- package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +17 -0
- package/dist/runtime/components/Shared/GitDeploymentSettings.vue +75 -0
- package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +17 -0
- package/dist/runtime/components/Shared/KpiTile.d.vue.ts +13 -0
- package/{components → dist/runtime/components}/Shared/KpiTile.vue +11 -19
- package/dist/runtime/components/Shared/KpiTile.vue.d.ts +13 -0
- 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/PageHeader.d.vue.ts +23 -0
- package/{components → dist/runtime/components}/Shared/PageHeader.vue +6 -8
- package/dist/runtime/components/Shared/PageHeader.vue.d.ts +23 -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 +41 -0
- package/dist/runtime/components/Shared/SidebarNav.vue +48 -0
- package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +41 -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/SortHeader.d.vue.ts +23 -0
- package/dist/runtime/components/Shared/SortHeader.vue +35 -0
- package/dist/runtime/components/Shared/SortHeader.vue.d.ts +23 -0
- package/dist/runtime/components/Shared/StateCard.d.vue.ts +32 -0
- package/{components → dist/runtime/components}/Shared/StateCard.vue +21 -36
- package/dist/runtime/components/Shared/StateCard.vue.d.ts +32 -0
- package/dist/runtime/components/Shared/StudioCard.d.vue.ts +15 -0
- package/dist/runtime/components/Shared/StudioCard.vue +19 -0
- package/dist/runtime/components/Shared/StudioCard.vue.d.ts +15 -0
- package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -0
- package/{components → dist/runtime/components}/Shared/TabBar.vue +7 -16
- package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -0
- package/dist/runtime/components/Shared/ViewTab.d.vue.ts +7 -0
- package/{components → dist/runtime/components}/Shared/ViewTab.vue +7 -9
- package/dist/runtime/components/Shared/ViewTab.vue.d.ts +7 -0
- package/dist/runtime/components/Shared/Wizard.d.vue.ts +47 -0
- package/dist/runtime/components/Shared/Wizard.vue +66 -0
- package/dist/runtime/components/Shared/Wizard.vue.d.ts +47 -0
- package/dist/runtime/composables/useListRail.d.ts +9 -0
- package/dist/runtime/composables/useListRail.js +19 -0
- package/dist/runtime/composables/useMediaLibrary.d.ts +41 -0
- package/dist/runtime/composables/useMediaLibrary.js +36 -0
- package/dist/runtime/composables/usePlatformAuth.d.ts +35 -0
- package/dist/runtime/composables/usePlatformAuth.js +17 -0
- package/dist/runtime/composables/usePlatformTalkback.d.ts +29 -0
- package/dist/runtime/composables/usePlatformTalkback.js +54 -0
- package/dist/runtime/composables/usePlatformTenant.d.ts +14 -0
- package/dist/runtime/composables/usePlatformTenant.js +18 -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 +37 -0
- package/dist/runtime/types/nav.js +0 -0
- package/dist/runtime/types/storage.d.ts +67 -0
- package/dist/runtime/types/storage.js +0 -0
- package/dist/types.d.mts +27 -0
- package/package.json +41 -13
- package/components/DataTable/DataTable.vue +0 -518
- package/components/DataTable/FilterMenu.vue +0 -40
- package/components/DataTable/types.ts +0 -82
- package/components/Shared/GitDeploymentSettings.vue +0 -87
- package/components/Shared/SidebarNav.vue +0 -100
- package/components/Shared/SortHeader.vue +0 -50
- package/components/Shared/StudioCard.vue +0 -24
- package/components/Shared/Wizard.vue +0 -80
- package/composables/usePlatformAuth.ts +0 -35
- package/composables/usePlatformTenant.ts +0 -35
- package/index.ts +0 -16
- package/nuxt.config.ts +0 -23
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Button } from "@revenexx/studio";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
steps: { type: Array, required: true },
|
|
5
|
+
modelValue: { type: Number, required: false, default: 0 },
|
|
6
|
+
canAdvance: { type: Boolean, required: false, default: true },
|
|
7
|
+
busy: { type: Boolean, required: false, default: false }
|
|
8
|
+
});
|
|
9
|
+
const emit = defineEmits(["update:modelValue", "finish"]);
|
|
10
|
+
const t = useT();
|
|
11
|
+
const index = computed({
|
|
12
|
+
get: () => props.modelValue,
|
|
13
|
+
set: (v) => emit("update:modelValue", v)
|
|
14
|
+
});
|
|
15
|
+
const isFirst = computed(() => index.value === 0);
|
|
16
|
+
const isLast = computed(() => index.value === props.steps.length - 1);
|
|
17
|
+
const activeStep = computed(() => props.steps[index.value]);
|
|
18
|
+
function back() {
|
|
19
|
+
if (!isFirst.value) index.value--;
|
|
20
|
+
}
|
|
21
|
+
function next() {
|
|
22
|
+
if (!props.canAdvance || props.busy) return;
|
|
23
|
+
if (isLast.value) emit("finish");
|
|
24
|
+
else index.value++;
|
|
25
|
+
}
|
|
26
|
+
function goTo(i) {
|
|
27
|
+
if (i < index.value) index.value = i;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="space-y-5">
|
|
33
|
+
<!-- Go-Live-style stepper: a fill bar + label per step, completed steps
|
|
34
|
+
are clickable to jump back. -->
|
|
35
|
+
<div class="flex gap-3">
|
|
36
|
+
<div
|
|
37
|
+
v-for="(s, i) in steps"
|
|
38
|
+
:key="s.key"
|
|
39
|
+
class="flex-1"
|
|
40
|
+
:class="i < index ? 'cursor-pointer' : ''"
|
|
41
|
+
@click="goTo(i)"
|
|
42
|
+
>
|
|
43
|
+
<div class="h-1 rounded-full transition-colors" :class="i <= index ? 'bg-primary' : 'bg-border'" />
|
|
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>
|
|
45
|
+
<div class="text-sm" :class="i <= index ? 'text-[#0B0522]' : 'text-muted-foreground'">{{ s.title }}</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div>
|
|
50
|
+
<slot :step="activeStep" :index="index" />
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="flex items-center justify-between border-t pt-3">
|
|
54
|
+
<!-- Custom footer (e.g. multiple finish actions on a review step). Falls
|
|
55
|
+
back to the standard Back / Next·Finish pair. -->
|
|
56
|
+
<slot name="footer" :back="back" :next="next" :is-first="isFirst" :is-last="isLast" :can-advance="canAdvance" :busy="busy" :index="index">
|
|
57
|
+
<Button variant="ghost" :disabled="isFirst || busy" @click="back">
|
|
58
|
+
{{ t("shared.wizard.action.back") }}
|
|
59
|
+
</Button>
|
|
60
|
+
<Button :disabled="!canAdvance || busy" class="gap-1" @click="next">
|
|
61
|
+
{{ isLast ? t("shared.wizard.action.finish") : t("shared.wizard.action.next") }}
|
|
62
|
+
</Button>
|
|
63
|
+
</slot>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface Step {
|
|
2
|
+
key: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
steps: Step[];
|
|
8
|
+
modelValue?: number;
|
|
9
|
+
/** Gate the Next/Finish button for the current step. */
|
|
10
|
+
canAdvance?: boolean;
|
|
11
|
+
busy?: boolean;
|
|
12
|
+
};
|
|
13
|
+
declare var __VLS_1: {
|
|
14
|
+
step: any;
|
|
15
|
+
index: any;
|
|
16
|
+
}, __VLS_3: {
|
|
17
|
+
back: any;
|
|
18
|
+
next: any;
|
|
19
|
+
isFirst: any;
|
|
20
|
+
isLast: any;
|
|
21
|
+
canAdvance: any;
|
|
22
|
+
busy: any;
|
|
23
|
+
index: any;
|
|
24
|
+
};
|
|
25
|
+
type __VLS_Slots = {} & {
|
|
26
|
+
default?: (props: typeof __VLS_1) => any;
|
|
27
|
+
} & {
|
|
28
|
+
footer?: (props: typeof __VLS_3) => any;
|
|
29
|
+
};
|
|
30
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
31
|
+
"update:modelValue": (args_0: number) => any;
|
|
32
|
+
finish: () => any;
|
|
33
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
34
|
+
"onUpdate:modelValue"?: ((args_0: number) => any) | undefined;
|
|
35
|
+
onFinish?: (() => any) | undefined;
|
|
36
|
+
}>, {
|
|
37
|
+
modelValue: number;
|
|
38
|
+
canAdvance: boolean;
|
|
39
|
+
busy: boolean;
|
|
40
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
41
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
42
|
+
export default _default;
|
|
43
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
44
|
+
new (): {
|
|
45
|
+
$slots: S;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Platform auth provider — the seam that lets a studio surface run either
|
|
4
|
+
* inside the full cockpit or standalone.
|
|
5
|
+
*
|
|
6
|
+
* This is the **standalone default**: it sources a bearer token from runtime
|
|
7
|
+
* config (`NUXT_PUBLIC_DEV_TOKEN`) or `localStorage['console.token']`. The
|
|
8
|
+
* full cockpit ships an override at `app/composables/usePlatformAuth.ts`
|
|
9
|
+
* (app-level auto-imports take precedence over this module's) that returns the
|
|
10
|
+
* real Appwrite/Console-sourced operator token.
|
|
11
|
+
*
|
|
12
|
+
* Studio modules (e.g. `@revenexx/studio-integrations`) call this instead of
|
|
13
|
+
* `useConsoleAuth` directly, so they carry no cockpit-only dependency.
|
|
14
|
+
*/
|
|
15
|
+
export interface PlatformAuth {
|
|
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>>;
|
|
34
|
+
}
|
|
35
|
+
export declare function usePlatformAuth(): PlatformAuth;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function usePlatformAuth() {
|
|
2
|
+
const config = useRuntimeConfig();
|
|
3
|
+
return {
|
|
4
|
+
async getToken() {
|
|
5
|
+
const fromConfig = config.public.devToken || null;
|
|
6
|
+
if (fromConfig) return fromConfig;
|
|
7
|
+
if (import.meta.client) {
|
|
8
|
+
try {
|
|
9
|
+
return localStorage.getItem("console.token");
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -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,14 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Platform tenant provider — companion to `usePlatformAuth`.
|
|
4
|
+
*
|
|
5
|
+
* Standalone default: the active tenant slug comes from runtime config
|
|
6
|
+
* (`NUXT_PUBLIC_DEV_TENANT`) or `localStorage['cockpit.devTenant']`. The full
|
|
7
|
+
* cockpit overrides this at `app/composables/usePlatformTenant.ts` to return
|
|
8
|
+
* the real active-tenant slug from `useConsoleTenants`.
|
|
9
|
+
*/
|
|
10
|
+
export interface PlatformTenant {
|
|
11
|
+
slug: Ref<string | null>;
|
|
12
|
+
load: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function usePlatformTenant(): PlatformTenant;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
export function usePlatformTenant() {
|
|
3
|
+
const config = useRuntimeConfig();
|
|
4
|
+
const stored = ref(null);
|
|
5
|
+
if (import.meta.client) {
|
|
6
|
+
try {
|
|
7
|
+
stored.value = localStorage.getItem("cockpit.devTenant");
|
|
8
|
+
} catch {
|
|
9
|
+
stored.value = null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const slug = computed(() => config.public.devTenant || stored.value);
|
|
13
|
+
return {
|
|
14
|
+
slug,
|
|
15
|
+
async load() {
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* What the host registers: both reactive, so a computed that formatted once
|
|
4
|
+
* re-runs when the operator changes the preference — no reload.
|
|
5
|
+
*/
|
|
6
|
+
export interface FormatPreferenceSource {
|
|
7
|
+
/** Resolved BCP-47 tag, e.g. `de-AT`. Never `'auto'` — the host resolves. */
|
|
8
|
+
locale: Ref<string>;
|
|
9
|
+
/**
|
|
10
|
+
* `true` forces a 12-hour clock, `false` a 24-hour one, `undefined` leaves
|
|
11
|
+
* the choice to the locale's own convention (which is what `Intl` does when
|
|
12
|
+
* the option is absent).
|
|
13
|
+
*/
|
|
14
|
+
hour12: Ref<boolean | undefined>;
|
|
15
|
+
}
|
|
16
|
+
/** Used when there is no host AND no i18n bridge. See the file comment. */
|
|
17
|
+
export declare const FALLBACK_FORMAT_LOCALE = "en-GB";
|
|
18
|
+
/**
|
|
19
|
+
* The region an `auto` preference resolves to for a UI language.
|
|
20
|
+
*
|
|
21
|
+
* Takes the language subtag, so `en-US` and `en` land on the same answer, and
|
|
22
|
+
* falls back to {@link FALLBACK_FORMAT_LOCALE} for a language we do not ship.
|
|
23
|
+
*/
|
|
24
|
+
export declare function defaultRegionForLanguage(language: string | null | undefined): string;
|
|
25
|
+
/** Called once by the host. Pass `null` to detach (tests). */
|
|
26
|
+
export declare function setFormatPreferences(source: FormatPreferenceSource | null): void;
|
|
27
|
+
/** Whether a host preference is installed. Rarely needed — prefer the getters. */
|
|
28
|
+
export declare function hasFormatPreferences(): boolean;
|
|
29
|
+
/** The BCP-47 tag every package should format in. */
|
|
30
|
+
export declare function formatLocale(): string;
|
|
31
|
+
/** The operator's clock, or `undefined` to let the locale decide. */
|
|
32
|
+
export declare function hour12(): boolean | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* A cached `DateTimeFormat` for an explicitly given locale and clock.
|
|
35
|
+
*
|
|
36
|
+
* The host uses this one: it has already resolved both from its own reactive
|
|
37
|
+
* state and must not have the answer re-derived from a registry it may not
|
|
38
|
+
* have populated yet. Packages want {@link dateTimeFormat}.
|
|
39
|
+
*
|
|
40
|
+
* `clock` is folded into the options rather than left to each call site, but an
|
|
41
|
+
* explicit `hour12` in `options` still wins — a cell that must print a 24-hour
|
|
42
|
+
* timestamp regardless (a log line, a cron preview) can say so.
|
|
43
|
+
*/
|
|
44
|
+
export declare function dateTimeFormatFor(locale: string, options?: Intl.DateTimeFormatOptions, clock?: boolean): Intl.DateTimeFormat;
|
|
45
|
+
/** A cached `NumberFormat` for an explicitly given locale. */
|
|
46
|
+
export declare function numberFormatFor(locale: string, options?: Intl.NumberFormatOptions): Intl.NumberFormat;
|
|
47
|
+
/** A `DateTimeFormat` for whatever the host registered. What packages call. */
|
|
48
|
+
export declare function dateTimeFormat(options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
|
|
49
|
+
/** A `NumberFormat` for whatever the host registered. What packages call. */
|
|
50
|
+
export declare function numberFormat(options?: Intl.NumberFormatOptions): Intl.NumberFormat;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { shallowRef } from "vue";
|
|
2
|
+
import { currentLocale } from "./i18nBridge.js";
|
|
3
|
+
export const FALLBACK_FORMAT_LOCALE = "en-GB";
|
|
4
|
+
const DEFAULT_REGION_BY_LANGUAGE = {
|
|
5
|
+
de: "de-DE",
|
|
6
|
+
en: "en-GB"
|
|
7
|
+
};
|
|
8
|
+
export function defaultRegionForLanguage(language) {
|
|
9
|
+
if (!language) return FALLBACK_FORMAT_LOCALE;
|
|
10
|
+
const subtag = String(language).split(/[-_]/)[0].toLowerCase();
|
|
11
|
+
return DEFAULT_REGION_BY_LANGUAGE[subtag] ?? FALLBACK_FORMAT_LOCALE;
|
|
12
|
+
}
|
|
13
|
+
const registered = shallowRef(null);
|
|
14
|
+
export function setFormatPreferences(source) {
|
|
15
|
+
registered.value = source;
|
|
16
|
+
}
|
|
17
|
+
export function hasFormatPreferences() {
|
|
18
|
+
return registered.value !== null;
|
|
19
|
+
}
|
|
20
|
+
export function formatLocale() {
|
|
21
|
+
const source = registered.value;
|
|
22
|
+
if (source) return source.locale.value;
|
|
23
|
+
return defaultRegionForLanguage(currentLocale());
|
|
24
|
+
}
|
|
25
|
+
export function hour12() {
|
|
26
|
+
return registered.value?.hour12.value;
|
|
27
|
+
}
|
|
28
|
+
const dateTimeFormats = /* @__PURE__ */ new Map();
|
|
29
|
+
const numberFormats = /* @__PURE__ */ new Map();
|
|
30
|
+
export function dateTimeFormatFor(locale, options = {}, clock) {
|
|
31
|
+
const resolved = clock === void 0 || "hour12" in options ? options : { ...options, hour12: clock };
|
|
32
|
+
const key = `${locale}|${JSON.stringify(resolved)}`;
|
|
33
|
+
let format = dateTimeFormats.get(key);
|
|
34
|
+
if (!format) {
|
|
35
|
+
format = new Intl.DateTimeFormat(locale, resolved);
|
|
36
|
+
dateTimeFormats.set(key, format);
|
|
37
|
+
}
|
|
38
|
+
return format;
|
|
39
|
+
}
|
|
40
|
+
export function numberFormatFor(locale, options = {}) {
|
|
41
|
+
const key = `${locale}|${JSON.stringify(options)}`;
|
|
42
|
+
let format = numberFormats.get(key);
|
|
43
|
+
if (!format) {
|
|
44
|
+
format = new Intl.NumberFormat(locale, options);
|
|
45
|
+
numberFormats.set(key, format);
|
|
46
|
+
}
|
|
47
|
+
return format;
|
|
48
|
+
}
|
|
49
|
+
export function dateTimeFormat(options = {}) {
|
|
50
|
+
return dateTimeFormatFor(formatLocale(), options, hour12());
|
|
51
|
+
}
|
|
52
|
+
export function numberFormat(options = {}) {
|
|
53
|
+
return numberFormatFor(formatLocale(), options);
|
|
54
|
+
}
|