@revenexx/studio-shared 0.2.0 → 0.3.1

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 (86) hide show
  1. package/dist/module.d.mts +7 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +37 -1
  4. package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -0
  5. package/dist/runtime/components/DataTable/ColumnsMenu.vue +93 -36
  6. package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -0
  7. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +53 -4
  8. package/dist/runtime/components/DataTable/DataTable.vue +274 -118
  9. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +53 -4
  10. package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +14 -3
  11. package/dist/runtime/components/DataTable/FilterMenu.vue +35 -29
  12. package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +14 -3
  13. package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -0
  14. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +189 -0
  15. package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -0
  16. package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +13 -0
  17. package/dist/runtime/components/DataTable/SortMenu.vue +47 -0
  18. package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +13 -0
  19. package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +20 -0
  20. package/dist/runtime/components/DataTable/ViewModeSwitch.vue +26 -0
  21. package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +20 -0
  22. package/dist/runtime/components/DataTable/types.d.ts +49 -2
  23. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +17 -0
  24. package/dist/runtime/components/Media/MediaFolderTree.vue +65 -0
  25. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +17 -0
  26. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +18 -0
  27. package/dist/runtime/components/Media/MediaPickerDialog.vue +163 -0
  28. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +18 -0
  29. package/dist/runtime/components/Media/MediaUploader.d.vue.ts +29 -0
  30. package/dist/runtime/components/Media/MediaUploader.vue +161 -0
  31. package/dist/runtime/components/Media/MediaUploader.vue.d.ts +29 -0
  32. package/dist/runtime/components/Shared/BuildLogsFlyout.vue +14 -13
  33. package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +55 -0
  34. package/dist/runtime/components/Shared/FloatingPanel.vue +118 -0
  35. package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +55 -0
  36. package/dist/runtime/components/Shared/GitDeploymentSettings.vue +11 -10
  37. package/dist/runtime/components/Shared/ListSurface.d.vue.ts +38 -0
  38. package/dist/runtime/components/Shared/ListSurface.vue +27 -0
  39. package/dist/runtime/components/Shared/ListSurface.vue.d.ts +38 -0
  40. package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +52 -0
  41. package/dist/runtime/components/Shared/RecordLayout.vue +63 -0
  42. package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +52 -0
  43. package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +26 -0
  44. package/dist/runtime/components/Shared/RecordRailCard.vue +20 -0
  45. package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +26 -0
  46. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +5 -0
  47. package/dist/runtime/components/Shared/SidebarNav.vue +6 -26
  48. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +5 -0
  49. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +15 -0
  50. package/dist/runtime/components/Shared/SidebarNavLink.vue +49 -0
  51. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +15 -0
  52. package/dist/runtime/components/Shared/Wizard.vue +6 -3
  53. package/dist/runtime/composables/useFloatingPanel.d.ts +69 -0
  54. package/dist/runtime/composables/useFloatingPanel.js +112 -0
  55. package/dist/runtime/composables/useListRail.d.ts +9 -0
  56. package/dist/runtime/composables/useListRail.js +19 -0
  57. package/dist/runtime/composables/useMediaLibrary.d.ts +41 -0
  58. package/dist/runtime/composables/useMediaLibrary.js +36 -0
  59. package/dist/runtime/composables/usePlatformAuth.d.ts +18 -0
  60. package/dist/runtime/composables/usePlatformTalkback.d.ts +29 -0
  61. package/dist/runtime/composables/usePlatformTalkback.js +54 -0
  62. package/dist/runtime/composables/useT.d.ts +11 -0
  63. package/dist/runtime/composables/useT.js +8 -0
  64. package/dist/runtime/composables/useTalkbackChannels.d.ts +93 -0
  65. package/dist/runtime/composables/useTalkbackChannels.js +56 -0
  66. package/dist/runtime/lib/columnOrder.d.ts +45 -0
  67. package/dist/runtime/lib/columnOrder.js +28 -0
  68. package/dist/runtime/lib/columnVisibility.d.ts +41 -0
  69. package/dist/runtime/lib/columnVisibility.js +12 -0
  70. package/dist/runtime/lib/dataTableFilters.d.ts +83 -0
  71. package/dist/runtime/lib/dataTableFilters.js +23 -0
  72. package/dist/runtime/lib/formatBridge.d.ts +50 -0
  73. package/dist/runtime/lib/formatBridge.js +54 -0
  74. package/dist/runtime/lib/i18nBridge.d.ts +36 -0
  75. package/dist/runtime/lib/i18nBridge.js +17 -0
  76. package/dist/runtime/lib/isoLists.d.ts +61 -0
  77. package/dist/runtime/lib/isoLists.js +62 -0
  78. package/dist/runtime/lib/sidebarNav.d.ts +34 -0
  79. package/dist/runtime/lib/sidebarNav.js +22 -0
  80. package/dist/runtime/lib/talkback.d.ts +59 -0
  81. package/dist/runtime/lib/talkback.js +49 -0
  82. package/dist/runtime/types/nav.d.ts +15 -1
  83. package/dist/runtime/types/storage.d.ts +67 -0
  84. package/dist/runtime/types/storage.js +0 -0
  85. package/dist/types.d.mts +14 -0
  86. package/package.json +19 -3
@@ -0,0 +1,69 @@
1
+ import { type Ref } from 'vue';
2
+ export type FloatingPanelDock = 'left' | 'right';
3
+ export type FloatingPanelMode = 'docked-left' | 'docked-right' | 'floating';
4
+ export interface FloatingPanelOptions {
5
+ /**
6
+ * Where this panel's placement is remembered. One key per panel — two panels
7
+ * sharing a key would drag each other around.
8
+ */
9
+ storageKey: string;
10
+ /** Which side it starts on, before anybody has moved it. */
11
+ initialMode?: FloatingPanelMode;
12
+ /** The panel's width, in px. Used to clamp it inside the canvas. */
13
+ width?: number;
14
+ /** Room to keep clear at the bottom, for a floating control bar. */
15
+ bottomClearance?: number;
16
+ }
17
+ /**
18
+ * A panel that can be docked to either side of a canvas, floated anywhere on
19
+ * it, or collapsed into a pill — and dragged between those states by its own
20
+ * header.
21
+ *
22
+ * Lifted from the integration studio's workflow editor, where this shape was
23
+ * worked out and where it has been in daily use: `usePaletteLayout`, with the
24
+ * storage key and the dimensions made arguments so a second editor can have
25
+ * its own panels rather than fighting over one remembered position.
26
+ *
27
+ * ## Why pointer events rather than HTML5 drag
28
+ *
29
+ * The panel's CONTENTS are draggable — the workflow editor drags nodes out of
30
+ * its palette onto the canvas, and the mail builder drags blocks. HTML5 drag
31
+ * on the shell would swallow those gestures. The header moves the panel with
32
+ * plain pointer events, so the two never compete: grab the bar, or grab a row,
33
+ * and each does the one thing.
34
+ *
35
+ * ## The magnet
36
+ *
37
+ * Coming within `DOCK_ZONE` of the left or right edge while dragging arms
38
+ * `dockPreview`, which the host renders as a ghost of the dock slot. Releasing
39
+ * inside the zone snaps into that dock; anywhere else leaves the panel where it
40
+ * was dropped. Docking is a state, not a position, so a docked panel keeps its
41
+ * side when the window resizes.
42
+ *
43
+ * Placement is persisted (guarded — private mode and quota must not throw) and
44
+ * clamped to the canvas on restore, because the window may have shrunk since.
45
+ */
46
+ export declare function useFloatingPanel(canvasRef: Ref<HTMLElement | null>, options: FloatingPanelOptions): {
47
+ mode: Ref<FloatingPanelMode, FloatingPanelMode>;
48
+ collapsed: Ref<boolean, boolean>;
49
+ floating: Ref<{
50
+ x: number;
51
+ y: number;
52
+ }, {
53
+ x: number;
54
+ y: number;
55
+ } | {
56
+ x: number;
57
+ y: number;
58
+ }>;
59
+ floatingStyle: import("vue").ComputedRef<{
60
+ left: string;
61
+ top: string;
62
+ }>;
63
+ dragging: Ref<boolean, boolean>;
64
+ dockPreview: Ref<FloatingPanelDock | null, FloatingPanelDock | null>;
65
+ startDrag: (ev: PointerEvent, shellEl: HTMLElement | null) => void;
66
+ dock: (side: FloatingPanelDock) => void;
67
+ floatHere: (shellEl: HTMLElement | null) => void;
68
+ setCollapsed: (v: boolean) => void;
69
+ };
@@ -0,0 +1,112 @@
1
+ import { computed, onBeforeUnmount, onMounted, ref } from "vue";
2
+ const MARGIN = 12;
3
+ const DOCK_ZONE = 48;
4
+ function clampNum(v, lo, hi) {
5
+ return Math.min(hi, Math.max(lo, v));
6
+ }
7
+ export function useFloatingPanel(canvasRef, options) {
8
+ const width = options.width ?? 320;
9
+ const bottomClearance = options.bottomClearance ?? 64;
10
+ const mode = ref(options.initialMode ?? "docked-left");
11
+ const collapsed = ref(false);
12
+ const floating = ref({ x: MARGIN, y: MARGIN });
13
+ const dragging = ref(false);
14
+ const dockPreview = ref(null);
15
+ function persist() {
16
+ try {
17
+ const payload = { mode: mode.value, floating: floating.value, collapsed: collapsed.value };
18
+ localStorage.setItem(options.storageKey, JSON.stringify(payload));
19
+ } catch {
20
+ }
21
+ }
22
+ function restore() {
23
+ try {
24
+ const raw = localStorage.getItem(options.storageKey);
25
+ if (!raw) return;
26
+ const saved = JSON.parse(raw);
27
+ if (saved.mode === "docked-left" || saved.mode === "docked-right" || saved.mode === "floating") {
28
+ mode.value = saved.mode;
29
+ }
30
+ if (typeof saved.floating?.x === "number" && typeof saved.floating?.y === "number") {
31
+ floating.value = { x: saved.floating.x, y: saved.floating.y };
32
+ }
33
+ collapsed.value = saved.collapsed === true;
34
+ clampFloating();
35
+ } catch {
36
+ }
37
+ }
38
+ function clampFloating() {
39
+ const rect = canvasRef.value?.getBoundingClientRect();
40
+ if (!rect || rect.width === 0) return;
41
+ floating.value = {
42
+ x: clampNum(floating.value.x, MARGIN, Math.max(MARGIN, rect.width - width - MARGIN)),
43
+ y: clampNum(floating.value.y, MARGIN, Math.max(MARGIN, rect.height - bottomClearance - 160))
44
+ };
45
+ }
46
+ onMounted(restore);
47
+ let grabDX = 0;
48
+ let grabDY = 0;
49
+ let panelHeight = 0;
50
+ function startDrag(ev, shellEl) {
51
+ if (ev.button !== 0 || dragging.value) return;
52
+ if (ev.target?.closest("button")) return;
53
+ const canvas = canvasRef.value;
54
+ if (!canvas || !shellEl) return;
55
+ const cRect = canvas.getBoundingClientRect();
56
+ const pRect = shellEl.getBoundingClientRect();
57
+ floating.value = { x: pRect.left - cRect.left, y: pRect.top - cRect.top };
58
+ mode.value = "floating";
59
+ grabDX = ev.clientX - pRect.left;
60
+ grabDY = ev.clientY - pRect.top;
61
+ panelHeight = Math.min(pRect.height, window.innerHeight * 0.7);
62
+ dragging.value = true;
63
+ ev.preventDefault();
64
+ window.addEventListener("pointermove", onDragMove);
65
+ window.addEventListener("pointerup", onDragEnd);
66
+ window.addEventListener("pointercancel", onDragEnd);
67
+ }
68
+ function onDragMove(ev) {
69
+ const rect = canvasRef.value?.getBoundingClientRect();
70
+ if (!rect) return;
71
+ const x = clampNum(ev.clientX - rect.left - grabDX, MARGIN, Math.max(MARGIN, rect.width - width - MARGIN));
72
+ const y = clampNum(ev.clientY - rect.top - grabDY, MARGIN, Math.max(MARGIN, rect.height - bottomClearance - panelHeight));
73
+ floating.value = { x, y };
74
+ dockPreview.value = x <= DOCK_ZONE ? "left" : x + width >= rect.width - DOCK_ZONE ? "right" : null;
75
+ }
76
+ function stopListeners() {
77
+ window.removeEventListener("pointermove", onDragMove);
78
+ window.removeEventListener("pointerup", onDragEnd);
79
+ window.removeEventListener("pointercancel", onDragEnd);
80
+ }
81
+ function onDragEnd() {
82
+ if (dockPreview.value) mode.value = dockPreview.value === "left" ? "docked-left" : "docked-right";
83
+ dockPreview.value = null;
84
+ dragging.value = false;
85
+ stopListeners();
86
+ persist();
87
+ }
88
+ onBeforeUnmount(stopListeners);
89
+ function dock(side) {
90
+ mode.value = side === "left" ? "docked-left" : "docked-right";
91
+ persist();
92
+ }
93
+ function floatHere(shellEl) {
94
+ if (mode.value !== "floating") {
95
+ const cRect = canvasRef.value?.getBoundingClientRect();
96
+ const pRect = shellEl?.getBoundingClientRect();
97
+ if (cRect && pRect) floating.value = { x: pRect.left - cRect.left, y: pRect.top - cRect.top };
98
+ mode.value = "floating";
99
+ clampFloating();
100
+ }
101
+ persist();
102
+ }
103
+ function setCollapsed(v) {
104
+ collapsed.value = v;
105
+ persist();
106
+ }
107
+ const floatingStyle = computed(() => ({
108
+ left: `${floating.value.x}px`,
109
+ top: `${floating.value.y}px`
110
+ }));
111
+ return { mode, collapsed, floating, floatingStyle, dragging, dockPreview, startDrag, dock, floatHere, setCollapsed };
112
+ }
@@ -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,93 @@
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. **An attach that cannot name a channel yet is retried, as long as what is
25
+ * missing is reactive.** `useChannel` attaches EAGERLY and only then installs
26
+ * the watch that would re-attach, and the eager call has no `try` around
27
+ * `resolveName()` while the watch's source does. So a name the grammar rejects
28
+ * at that moment — overwhelmingly a tenant that has not been loaded yet, since
29
+ * it is positionally first in every channel name — throws straight out of the
30
+ * composable, before anything exists that could try again. Swallowing that (as
31
+ * this wrapper once did) turns "not yet" into "never": the subscription is lost
32
+ * for the life of the tab while the socket connects perfectly well, which is
33
+ * the shape realtime silently missing takes. The attach therefore runs inside
34
+ * an effect, so the reads that got as far as throwing are tracked and the
35
+ * attempt is repeated when one of them changes.
36
+ *
37
+ * THE QUALIFIER IS LOAD-BEARING, and `useTalkbackUser` is where it bites:
38
+ * `userChannel(tenant, '')` throws too, but the subject is a plain `let userId`
39
+ * in `createPlatformTalkback` — cached from the last mint so it can answer
40
+ * synchronously — so the tenant is the only tracked read and a subject that
41
+ * fills in later re-runs nothing. That is not new (the throw was swallowed
42
+ * before as well, and there was no channel either way), and a null token is the
43
+ * ordinary state for up to fifteen seconds per `usePlatformTalkback`. Making
44
+ * `userId` reactive is what would close it.
45
+ *
46
+ * With realtime off (`talkbackHost` empty) every one of these is a no-op that
47
+ * reports a null channel, so a call site needs no `if (enabled)`.
48
+ */
49
+ /** Re-exported so a consumer types a ref-able argument without importing talkback-js. */
50
+ export type TalkbackMaybeRef<T> = MaybeRefOrGetter<T>;
51
+ export interface TalkbackChannelOptions {
52
+ /** One action, or several. Omit to receive every action on the channel. */
53
+ on?: string | readonly string[];
54
+ /** Called for each matching envelope. Events are refetch signals, not state. */
55
+ handler?: EnvelopeListener;
56
+ /** Every publication, envelope or not — how a `stream:` channel is read. */
57
+ raw?: RawListener;
58
+ /** The recovery buffer could not close the gap: refetch over HTTP. */
59
+ onResync?: (ctx: ResyncContext) => void;
60
+ /** Skip subscribing, e.g. while a route parameter is still empty. */
61
+ enabled?: TalkbackMaybeRef<boolean>;
62
+ }
63
+ export interface TalkbackChannel {
64
+ /** The currently subscribed channel, or null when nothing is subscribed. */
65
+ channel: ComputedRef<string | null>;
66
+ /** Releases early. Called automatically when the calling scope is disposed. */
67
+ stop: () => void;
68
+ }
69
+ /**
70
+ * The ACTION channel for a topic — a grid watching every instance of a kind.
71
+ *
72
+ * THE TOPIC INCLUDES THE ACTION, and one action is one channel (Centrifugo has
73
+ * no wildcards), so a grid watching three actions takes three calls:
74
+ *
75
+ * ```ts
76
+ * for (const action of ['started', 'finished', 'failed'] as const) {
77
+ * useTalkbackTopic(`revenexx.integrations.run.${action}`, { handler: () => load(true) })
78
+ * }
79
+ * ```
80
+ */
81
+ export declare function useTalkbackTopic(topic: TalkbackMaybeRef<string>, options?: TalkbackChannelOptions): TalkbackChannel;
82
+ /**
83
+ * The RESOURCE channel — one detail view, every action on it. The id may be a
84
+ * ref or a getter, and the subscription follows it, releasing the old channel
85
+ * before taking the new one.
86
+ */
87
+ export declare function useTalkbackResource(topic: TalkbackMaybeRef<string>, topicId: TalkbackMaybeRef<string | null | undefined>, options?: TalkbackChannelOptions): TalkbackChannel;
88
+ /** The signed-in user's own channel — notifications and toasts. */
89
+ export declare function useTalkbackUser(options?: TalkbackChannelOptions): TalkbackChannel;
90
+ /** A `stream:` channel. `onResync` fires on EVERY subscribe here — it keeps no history. */
91
+ export declare function useTalkbackStream(streamId: TalkbackMaybeRef<string | null | undefined>, options?: TalkbackChannelOptions): TalkbackChannel;
92
+ /** Who else is looking at this resource. */
93
+ export declare function useTalkbackPresence(vendor: TalkbackMaybeRef<string>, app: TalkbackMaybeRef<string>, entity: TalkbackMaybeRef<string>, id?: TalkbackMaybeRef<string | null | undefined>, options?: TalkbackChannelOptions): TalkbackChannel;
@@ -0,0 +1,56 @@
1
+ import { computed, effectScope, onScopeDispose, shallowRef, watch, watchEffect } from "vue";
2
+ import { talkbackRef } from "../lib/talkback.js";
3
+ function useDelegate(subscribe) {
4
+ const client = talkbackRef();
5
+ const attached = shallowRef(null);
6
+ const scope = shallowRef(null);
7
+ function stop() {
8
+ scope.value?.stop();
9
+ scope.value = null;
10
+ attached.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 = null;
22
+ watchEffect(() => {
23
+ if (sub) return;
24
+ child.run(() => {
25
+ try {
26
+ sub = subscribe(vue, tb);
27
+ } catch {
28
+ return;
29
+ }
30
+ attached.value = sub;
31
+ });
32
+ });
33
+ });
34
+ })();
35
+ }, { immediate: true });
36
+ onScopeDispose(() => {
37
+ unwatch();
38
+ stop();
39
+ });
40
+ return { channel: computed(() => attached.value?.channel.value ?? null), stop };
41
+ }
42
+ export function useTalkbackTopic(topic, options = {}) {
43
+ return useDelegate((vue, tb) => vue.useTalkbackTopic(topic, { ...options, talkback: tb }));
44
+ }
45
+ export function useTalkbackResource(topic, topicId, options = {}) {
46
+ return useDelegate((vue, tb) => vue.useTalkbackResource(topic, topicId, { ...options, talkback: tb }));
47
+ }
48
+ export function useTalkbackUser(options = {}) {
49
+ return useDelegate((vue, tb) => vue.useTalkbackUser({ ...options, talkback: tb }));
50
+ }
51
+ export function useTalkbackStream(streamId, options = {}) {
52
+ return useDelegate((vue, tb) => vue.useTalkbackStream(streamId, { ...options, talkback: tb }));
53
+ }
54
+ export function useTalkbackPresence(vendor, app, entity, id = () => null, options = {}) {
55
+ return useDelegate((vue, tb) => vue.useTalkbackPresence(vendor, app, entity, id, { ...options, talkback: tb }));
56
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The order a table's columns are drawn in, once the reader has an opinion.
3
+ *
4
+ * Sits beside `columnVisibility` because it has the SAME trap, and the trap has
5
+ * already shipped once in this file's neighbour.
6
+ *
7
+ * A table's column list is not fixed for the life of the component. A tab that
8
+ * offers Markets only once the service proves it has the column, or engagement
9
+ * once the deployment reports any, hands the table a LONGER array on a later
10
+ * render. If the remembered order were treated as the whole truth, every column
11
+ * that turned up afterwards would be missing from it — and the obvious
12
+ * implementation, "sort by index in the remembered list, unknowns last", puts
13
+ * that new column silently at the far right where nobody scrolls. Which is
14
+ * indistinguishable, from the outside, from never having been built.
15
+ *
16
+ * So what is remembered is the reader's own arrangement, and a column that is
17
+ * not in it keeps its DECLARED neighbours: it lands directly after whichever
18
+ * declared predecessor is nearest, or first if it has none. A column added
19
+ * between Channel and Status appears between Channel and Status, wherever the
20
+ * reader has since dragged those two.
21
+ */
22
+ export interface OrderableColumn {
23
+ key: string;
24
+ }
25
+ /**
26
+ * The declared columns, arranged.
27
+ *
28
+ * An empty `order` means the reader has not touched anything, which is not the
29
+ * same as an order that happens to match — the first is "use the declaration",
30
+ * the second is a decision that must survive the declaration changing.
31
+ */
32
+ export declare function orderColumnsBy<T extends OrderableColumn>(columns: readonly T[], order: readonly string[]): T[];
33
+ /**
34
+ * Move one column and return the arrangement to remember.
35
+ *
36
+ * Always a COMPLETE list of keys, even when the previous one was partial or
37
+ * empty: after the first drag there is an opinion about every column, and a
38
+ * half-remembered order would leave the untouched ones re-deriving their
39
+ * position on every render.
40
+ *
41
+ * `to` is the index in the list as it will be — what a drop target means — and
42
+ * out-of-range values clamp rather than throw. A drag that ends outside the
43
+ * list means "leave it where it is", not "crash the picker".
44
+ */
45
+ export declare function reorderColumns<T extends OrderableColumn>(columns: readonly T[], order: readonly string[], from: number, to: number): string[];
@@ -0,0 +1,28 @@
1
+ export function orderColumnsBy(columns, order) {
2
+ if (!order.length) return [...columns];
3
+ const byKey = new Map(columns.map((c) => [c.key, c]));
4
+ const remembered = new Set(order);
5
+ const result = order.filter((k) => byKey.has(k)).map((k) => byKey.get(k));
6
+ columns.forEach((column, declaredIndex) => {
7
+ if (remembered.has(column.key)) return;
8
+ let insertAt = 0;
9
+ for (let i = declaredIndex - 1; i >= 0; i--) {
10
+ const at = result.indexOf(columns[i]);
11
+ if (at !== -1) {
12
+ insertAt = at + 1;
13
+ break;
14
+ }
15
+ }
16
+ result.splice(insertAt, 0, column);
17
+ });
18
+ return result;
19
+ }
20
+ export function reorderColumns(columns, order, from, to) {
21
+ const keys = orderColumnsBy(columns, order).map((c) => c.key);
22
+ if (from < 0 || from >= keys.length) return keys;
23
+ const clamped = Math.max(0, Math.min(to, keys.length - 1));
24
+ if (clamped === from) return keys;
25
+ const [moved] = keys.splice(from, 1);
26
+ keys.splice(clamped, 0, moved);
27
+ return keys;
28
+ }