@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.
Files changed (73) hide show
  1. package/dist/module.d.mts +6 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +32 -1
  4. package/dist/runtime/components/DataTable/ColumnsMenu.vue +3 -2
  5. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +32 -4
  6. package/dist/runtime/components/DataTable/DataTable.vue +91 -34
  7. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +32 -4
  8. package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +12 -3
  9. package/dist/runtime/components/DataTable/FilterMenu.vue +20 -10
  10. package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +12 -3
  11. package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -0
  12. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +189 -0
  13. package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -0
  14. package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +13 -0
  15. package/dist/runtime/components/DataTable/SortMenu.vue +47 -0
  16. package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +13 -0
  17. package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +20 -0
  18. package/dist/runtime/components/DataTable/ViewModeSwitch.vue +26 -0
  19. package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +20 -0
  20. package/dist/runtime/components/DataTable/types.d.ts +34 -0
  21. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +17 -0
  22. package/dist/runtime/components/Media/MediaFolderTree.vue +65 -0
  23. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +17 -0
  24. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +18 -0
  25. package/dist/runtime/components/Media/MediaPickerDialog.vue +163 -0
  26. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +18 -0
  27. package/dist/runtime/components/Media/MediaUploader.d.vue.ts +29 -0
  28. package/dist/runtime/components/Media/MediaUploader.vue +161 -0
  29. package/dist/runtime/components/Media/MediaUploader.vue.d.ts +29 -0
  30. package/dist/runtime/components/Shared/BuildLogsFlyout.vue +14 -13
  31. package/dist/runtime/components/Shared/GitDeploymentSettings.vue +11 -10
  32. package/dist/runtime/components/Shared/ListSurface.d.vue.ts +38 -0
  33. package/dist/runtime/components/Shared/ListSurface.vue +27 -0
  34. package/dist/runtime/components/Shared/ListSurface.vue.d.ts +38 -0
  35. package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +52 -0
  36. package/dist/runtime/components/Shared/RecordLayout.vue +63 -0
  37. package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +52 -0
  38. package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +26 -0
  39. package/dist/runtime/components/Shared/RecordRailCard.vue +20 -0
  40. package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +26 -0
  41. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +5 -0
  42. package/dist/runtime/components/Shared/SidebarNav.vue +6 -26
  43. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +5 -0
  44. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +15 -0
  45. package/dist/runtime/components/Shared/SidebarNavLink.vue +49 -0
  46. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +15 -0
  47. package/dist/runtime/components/Shared/Wizard.vue +6 -3
  48. package/dist/runtime/composables/useListRail.d.ts +9 -0
  49. package/dist/runtime/composables/useListRail.js +19 -0
  50. package/dist/runtime/composables/useMediaLibrary.d.ts +41 -0
  51. package/dist/runtime/composables/useMediaLibrary.js +36 -0
  52. package/dist/runtime/composables/usePlatformAuth.d.ts +18 -0
  53. package/dist/runtime/composables/usePlatformTalkback.d.ts +29 -0
  54. package/dist/runtime/composables/usePlatformTalkback.js +54 -0
  55. package/dist/runtime/composables/useT.d.ts +11 -0
  56. package/dist/runtime/composables/useT.js +8 -0
  57. package/dist/runtime/composables/useTalkbackChannels.d.ts +78 -0
  58. package/dist/runtime/composables/useTalkbackChannels.js +52 -0
  59. package/dist/runtime/lib/formatBridge.d.ts +50 -0
  60. package/dist/runtime/lib/formatBridge.js +54 -0
  61. package/dist/runtime/lib/i18nBridge.d.ts +31 -0
  62. package/dist/runtime/lib/i18nBridge.js +17 -0
  63. package/dist/runtime/lib/isoLists.d.ts +61 -0
  64. package/dist/runtime/lib/isoLists.js +62 -0
  65. package/dist/runtime/lib/sidebarNav.d.ts +34 -0
  66. package/dist/runtime/lib/sidebarNav.js +22 -0
  67. package/dist/runtime/lib/talkback.d.ts +59 -0
  68. package/dist/runtime/lib/talkback.js +49 -0
  69. package/dist/runtime/types/nav.d.ts +15 -1
  70. package/dist/runtime/types/storage.d.ts +67 -0
  71. package/dist/runtime/types/storage.js +0 -0
  72. package/dist/types.d.mts +12 -0
  73. 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
- <NuxtLink
37
+ <SidebarNavLink
41
38
  v-for="item in section.items"
42
- :key="item.to"
43
- :to="item.to"
44
- class="group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors hover:text-primary"
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'">Step {{ i + 1 }}</div>
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">Back</Button>
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 ? "Finish" : "Next" }}
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,8 @@
1
+ import { computed } from "vue";
2
+ import { currentLocale, translate } from "../lib/i18nBridge.js";
3
+ export function useT() {
4
+ return translate;
5
+ }
6
+ export function useUiLocale() {
7
+ return computed(() => currentLocale());
8
+ }
@@ -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
+ }