@revenexx/studio-shared 0.3.2 → 0.4.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 (166) hide show
  1. package/dist/module.d.mts +6 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +12 -1
  4. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.d.vue.ts +16 -0
  5. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +39 -0
  6. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue.d.ts +16 -0
  7. package/dist/runtime/components/Dashboard/DashboardControls.d.vue.ts +58 -0
  8. package/dist/runtime/components/Dashboard/DashboardControls.vue +209 -0
  9. package/dist/runtime/components/Dashboard/DashboardControls.vue.d.ts +58 -0
  10. package/dist/runtime/components/Dashboard/DashboardGrid.d.vue.ts +17 -0
  11. package/dist/runtime/components/Dashboard/DashboardGrid.vue +87 -0
  12. package/dist/runtime/components/Dashboard/DashboardGrid.vue.d.ts +17 -0
  13. package/dist/runtime/components/Dashboard/DashboardNameDialog.d.vue.ts +19 -0
  14. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +61 -0
  15. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue.d.ts +19 -0
  16. package/dist/runtime/components/Dashboard/DashboardPickerDialog.d.vue.ts +15 -0
  17. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +100 -0
  18. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue.d.ts +15 -0
  19. package/dist/runtime/components/Dashboard/DashboardShareDialog.d.vue.ts +28 -0
  20. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +155 -0
  21. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue.d.ts +28 -0
  22. package/dist/runtime/components/Dashboard/DashboardTopBar.d.vue.ts +64 -0
  23. package/dist/runtime/components/Dashboard/DashboardTopBar.vue +79 -0
  24. package/dist/runtime/components/Dashboard/DashboardTopBar.vue.d.ts +64 -0
  25. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.d.vue.ts +44 -0
  26. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +153 -0
  27. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue.d.ts +44 -0
  28. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +28 -0
  29. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +80 -0
  30. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +28 -0
  31. package/dist/runtime/components/Dashboard/DashboardWidgetHost.d.vue.ts +7 -0
  32. package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue +30 -0
  33. package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue.d.ts +7 -0
  34. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.d.vue.ts +9 -0
  35. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue +13 -0
  36. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue.d.ts +9 -0
  37. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.d.vue.ts +24 -0
  38. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue +140 -0
  39. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue.d.ts +24 -0
  40. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.d.vue.ts +23 -0
  41. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue +124 -0
  42. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue.d.ts +23 -0
  43. package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -1
  44. package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -1
  45. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +105 -28
  46. package/dist/runtime/components/DataTable/DataTable.vue +403 -38
  47. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +105 -28
  48. package/dist/runtime/components/DataTable/FacetChips.d.vue.ts +17 -0
  49. package/dist/runtime/components/DataTable/FacetChips.vue +72 -0
  50. package/dist/runtime/components/DataTable/FacetChips.vue.d.ts +17 -0
  51. package/dist/runtime/components/DataTable/FacetPanel.d.vue.ts +21 -0
  52. package/dist/runtime/components/DataTable/FacetPanel.vue +359 -0
  53. package/dist/runtime/components/DataTable/FacetPanel.vue.d.ts +21 -0
  54. package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +6 -5
  55. package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +6 -5
  56. package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -1
  57. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +83 -63
  58. package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -1
  59. package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +9 -3
  60. package/dist/runtime/components/DataTable/SortMenu.vue +40 -12
  61. package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +9 -3
  62. package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +4 -2
  63. package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +4 -2
  64. package/dist/runtime/components/Form/FieldCell.d.vue.ts +32 -0
  65. package/dist/runtime/components/Form/FieldCell.vue +46 -0
  66. package/dist/runtime/components/Form/FieldCell.vue.d.ts +32 -0
  67. package/dist/runtime/components/Form/FieldGrid.d.vue.ts +13 -0
  68. package/dist/runtime/components/Form/FieldGrid.vue +11 -0
  69. package/dist/runtime/components/Form/FieldGrid.vue.d.ts +13 -0
  70. package/dist/runtime/components/Form/FormContextCard.d.vue.ts +33 -0
  71. package/dist/runtime/components/Form/FormContextCard.vue +56 -0
  72. package/dist/runtime/components/Form/FormContextCard.vue.d.ts +33 -0
  73. package/dist/runtime/components/Form/FormLayout.d.vue.ts +47 -0
  74. package/dist/runtime/components/Form/FormLayout.vue +169 -0
  75. package/dist/runtime/components/Form/FormLayout.vue.d.ts +47 -0
  76. package/dist/runtime/components/Form/FormSection.d.vue.ts +25 -0
  77. package/dist/runtime/components/Form/FormSection.vue +48 -0
  78. package/dist/runtime/components/Form/FormSection.vue.d.ts +25 -0
  79. package/dist/runtime/components/Form/SaveBar.d.vue.ts +44 -0
  80. package/dist/runtime/components/Form/SaveBar.vue +71 -0
  81. package/dist/runtime/components/Form/SaveBar.vue.d.ts +44 -0
  82. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +2 -1
  83. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +2 -1
  84. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +4 -3
  85. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +4 -3
  86. package/dist/runtime/components/Media/MediaUploader.d.vue.ts +4 -3
  87. package/dist/runtime/components/Media/MediaUploader.vue.d.ts +4 -3
  88. package/dist/runtime/components/Shared/AppIcon.d.vue.ts +21 -0
  89. package/dist/runtime/components/Shared/AppIcon.vue +47 -0
  90. package/dist/runtime/components/Shared/AppIcon.vue.d.ts +21 -0
  91. package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +2 -1
  92. package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +2 -1
  93. package/dist/runtime/components/Shared/DetailSheet.d.vue.ts +60 -0
  94. package/dist/runtime/components/Shared/DetailSheet.vue +207 -0
  95. package/dist/runtime/components/Shared/DetailSheet.vue.d.ts +60 -0
  96. package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +7 -6
  97. package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +7 -6
  98. package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +2 -1
  99. package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +2 -1
  100. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +2 -1
  101. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +2 -1
  102. package/dist/runtime/components/Shared/ListSurface.d.vue.ts +3 -2
  103. package/dist/runtime/components/Shared/ListSurface.vue.d.ts +3 -2
  104. package/dist/runtime/components/Shared/PageHeader.d.vue.ts +7 -6
  105. package/dist/runtime/components/Shared/PageHeader.vue.d.ts +7 -6
  106. package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +8 -7
  107. package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +8 -7
  108. package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +3 -2
  109. package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +3 -2
  110. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +3 -2
  111. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +3 -2
  112. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +2 -1
  113. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +2 -1
  114. package/dist/runtime/components/Shared/SortHeader.d.vue.ts +3 -2
  115. package/dist/runtime/components/Shared/SortHeader.vue +6 -2
  116. package/dist/runtime/components/Shared/SortHeader.vue.d.ts +3 -2
  117. package/dist/runtime/components/Shared/StateCard.d.vue.ts +6 -5
  118. package/dist/runtime/components/Shared/StateCard.vue.d.ts +6 -5
  119. package/dist/runtime/components/Shared/StudioCard.d.vue.ts +5 -4
  120. package/dist/runtime/components/Shared/StudioCard.vue.d.ts +5 -4
  121. package/dist/runtime/components/Shared/TabBar.d.vue.ts +9 -10
  122. package/dist/runtime/components/Shared/TabBar.vue +26 -7
  123. package/dist/runtime/components/Shared/TabBar.vue.d.ts +9 -10
  124. package/dist/runtime/components/Shared/ViewTab.d.vue.ts +2 -1
  125. package/dist/runtime/components/Shared/ViewTab.vue.d.ts +2 -1
  126. package/dist/runtime/components/Shared/Wizard.d.vue.ts +10 -7
  127. package/dist/runtime/components/Shared/Wizard.vue.d.ts +10 -7
  128. package/dist/runtime/composables/useDashboards.d.ts +27 -0
  129. package/dist/runtime/composables/useDashboards.js +143 -0
  130. package/dist/runtime/composables/useDetailSheet.d.ts +20 -0
  131. package/dist/runtime/composables/useDetailSheet.js +50 -0
  132. package/dist/runtime/composables/usePlatformTenant.d.ts +6 -0
  133. package/dist/runtime/composables/useSavedViews.d.ts +44 -0
  134. package/dist/runtime/composables/useSavedViews.js +44 -0
  135. package/dist/runtime/composables/useStickyListView.d.ts +60 -0
  136. package/dist/runtime/composables/useStickyListView.js +32 -0
  137. package/dist/runtime/lib/activeTenant.d.ts +58 -0
  138. package/dist/runtime/lib/activeTenant.js +45 -0
  139. package/dist/runtime/lib/appIcon.d.ts +48 -0
  140. package/dist/runtime/lib/appIcon.js +27 -0
  141. package/dist/runtime/lib/canonicalJson.d.ts +43 -0
  142. package/dist/runtime/lib/canonicalJson.js +38 -0
  143. package/dist/runtime/lib/dashboardIcons.d.ts +4 -0
  144. package/dist/runtime/lib/dashboardIcons.js +50 -0
  145. package/dist/runtime/lib/detailSheet.d.ts +43 -0
  146. package/dist/runtime/lib/detailSheet.js +34 -0
  147. package/dist/runtime/lib/facets.d.ts +200 -0
  148. package/dist/runtime/lib/facets.js +171 -0
  149. package/dist/runtime/lib/formLayout.d.ts +94 -0
  150. package/dist/runtime/lib/formLayout.js +70 -0
  151. package/dist/runtime/lib/formLayoutContext.d.ts +26 -0
  152. package/dist/runtime/lib/formLayoutContext.js +1 -0
  153. package/dist/runtime/lib/gridTracks.d.ts +55 -0
  154. package/dist/runtime/lib/gridTracks.js +29 -0
  155. package/dist/runtime/lib/rowMove.d.ts +38 -0
  156. package/dist/runtime/lib/rowMove.js +20 -0
  157. package/dist/runtime/lib/solarIconMap.d.ts +15 -0
  158. package/dist/runtime/lib/solarIconMap.js +280 -0
  159. package/dist/runtime/lib/solarIcons.d.ts +37 -0
  160. package/dist/runtime/lib/solarIcons.js +25 -0
  161. package/dist/runtime/lib/widgetRegistry.d.ts +124 -0
  162. package/dist/runtime/lib/widgetRegistry.js +66 -0
  163. package/dist/runtime/types/dashboard.d.ts +95 -0
  164. package/dist/runtime/types/dashboard.js +2 -0
  165. package/dist/types.d.mts +12 -0
  166. package/package.json +3 -2
@@ -0,0 +1,20 @@
1
+ import type { ComputedRef, MaybeRefOrGetter, WritableComputedRef } from 'vue';
2
+ export interface DetailSheetBinding {
3
+ /** The open record's id, or `null` when the panel is shut. */
4
+ openId: ComputedRef<string | null>;
5
+ /** For `v-model:open` — writing `false` closes. */
6
+ isOpen: WritableComputedRef<boolean>;
7
+ open: (id: string) => void;
8
+ close: () => void;
9
+ /** Move to the neighbouring record in `ids`; a no-op at either end. */
10
+ step: (dir: -1 | 1) => void;
11
+ /** Whether `step(-1)` / `step(1)` would land anywhere — for the panel's arrow buttons. */
12
+ canStep: ComputedRef<{
13
+ prev: boolean;
14
+ next: boolean;
15
+ }>;
16
+ }
17
+ export declare function useDetailSheet(key: string, options?: {
18
+ /** The records in the order the list shows them, for `step`. */
19
+ ids?: MaybeRefOrGetter<readonly string[]>;
20
+ }): DetailSheetBinding;
@@ -0,0 +1,50 @@
1
+ import { computed, toValue, watch } from "vue";
2
+ import { idFromQuery, neighbourId, queryWith } from "../lib/detailSheet.js";
3
+ const MARKER = "detailSheet";
4
+ export function useDetailSheet(key, options = {}) {
5
+ const route = useRoute();
6
+ const router = useRouter();
7
+ const openId = computed(() => idFromQuery(route.query[key]));
8
+ function pushedHere() {
9
+ if (typeof window === "undefined") return false;
10
+ return window.history.state?.[MARKER] === key;
11
+ }
12
+ let closing = false;
13
+ watch(openId, (id) => {
14
+ if (id === null) closing = false;
15
+ });
16
+ function open(id) {
17
+ if (openId.value === id) return;
18
+ closing = false;
19
+ const query = queryWith(route.query, key, id);
20
+ if (openId.value === null) {
21
+ void router.push({ query, state: { [MARKER]: key } });
22
+ } else {
23
+ void router.replace({ query, state: pushedHere() ? { [MARKER]: key } : {} });
24
+ }
25
+ }
26
+ function close() {
27
+ if (openId.value === null || closing) return;
28
+ closing = true;
29
+ if (pushedHere()) router.back();
30
+ else void router.replace({ query: queryWith(route.query, key, null) });
31
+ }
32
+ function step(dir) {
33
+ const next = neighbourId(toValue(options.ids) ?? [], openId.value, dir);
34
+ if (next !== null) open(next);
35
+ }
36
+ const canStep = computed(() => {
37
+ const ids = toValue(options.ids) ?? [];
38
+ return {
39
+ prev: neighbourId(ids, openId.value, -1) !== null,
40
+ next: neighbourId(ids, openId.value, 1) !== null
41
+ };
42
+ });
43
+ const isOpen = computed({
44
+ get: () => openId.value !== null,
45
+ set: (v) => {
46
+ if (!v) close();
47
+ }
48
+ });
49
+ return { openId, isOpen, open, close, step, canStep };
50
+ }
@@ -9,6 +9,12 @@ import type { Ref } from 'vue';
9
9
  */
10
10
  export interface PlatformTenant {
11
11
  slug: Ref<string | null>;
12
+ /**
13
+ * Resolves once the active tenant is known, or once it is clear there is
14
+ * none yet (bounded — the host decides how long). Never throws. Callers that
15
+ * send tenant-scoped requests use `requirePlatformTenant` from
16
+ * `@revenexx/studio-shared/lib/activeTenant` instead of reading `slug`.
17
+ */
12
18
  load: () => Promise<void>;
13
19
  }
14
20
  export declare function usePlatformTenant(): PlatformTenant;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Saved-views storage provider — the DEFAULT (PE-608).
3
+ *
4
+ * ## Why this is a seam and not just an API call
5
+ *
6
+ * `SavedViewsMenu` ships inside `@revenexx/studio-shared`, which is published
7
+ * into hosts that have no cockpit backend: the standalone Commerce Studio dev
8
+ * host, and anything else that installs the module for its DataTable. So the
9
+ * component cannot reach for `/api/cockpit/saved-views` — it would 404 in every
10
+ * host but one, and the control would be permanently broken there.
11
+ *
12
+ * Same shape as `usePlatformAuth`, `usePlatformTenant` and `useMediaLibrary`:
13
+ * this file is the default, registered at `priority: -1`, and the cockpit ships
14
+ * a same-named app-level composable that wins. Here, views are kept in
15
+ * `localStorage`, which is exactly what the menu did before — so a bare host
16
+ * behaves as it always has, and only the cockpit gains the account-backed copy.
17
+ *
18
+ * ## Why `id` exists in a localStorage implementation
19
+ *
20
+ * The server keys a view by a uuid and the menu deletes by it. Making the local
21
+ * store agree means the component has one code path instead of two, and a
22
+ * `name`-keyed delete cannot be quietly wrong about which of two views with the
23
+ * same name it removed. The id is derived from the surface and the name, which
24
+ * is stable across reloads and is all the uniqueness a browser-local store has.
25
+ */
26
+ /** One named snapshot of how a list was narrowed. */
27
+ export interface StoredSavedView {
28
+ id: string;
29
+ name: string;
30
+ snapshot: unknown;
31
+ }
32
+ export interface SavedViewsStore {
33
+ /** Read this surface's views. Never throws — a failure is an empty list. */
34
+ list: (surface: string) => Promise<StoredSavedView[]>;
35
+ /** Save under a name, replacing what that name held. */
36
+ save: (surface: string, name: string, snapshot: unknown) => Promise<StoredSavedView>;
37
+ remove: (surface: string, id: string) => Promise<void>;
38
+ /**
39
+ * Does this store survive a new browser? The menu says so in one line, so
40
+ * "saved to your account" and "saved in this browser only" stay honest.
41
+ */
42
+ readonly durable: boolean;
43
+ }
44
+ export declare function useSavedViews(): SavedViewsStore;
@@ -0,0 +1,44 @@
1
+ function localId(surface, name) {
2
+ return `local:${surface}::${name}`;
3
+ }
4
+ function readAll(surface) {
5
+ try {
6
+ if (typeof window === "undefined") return [];
7
+ const raw = window.localStorage.getItem(surface);
8
+ if (!raw) return [];
9
+ const parsed = JSON.parse(raw);
10
+ if (!Array.isArray(parsed)) return [];
11
+ return parsed.filter((v) => !!v && typeof v === "object" && typeof v.name === "string").map((v) => ({ id: localId(surface, v.name), name: v.name, snapshot: v.snapshot }));
12
+ } catch {
13
+ return [];
14
+ }
15
+ }
16
+ function writeAll(surface, views) {
17
+ try {
18
+ if (typeof window === "undefined") return;
19
+ window.localStorage.setItem(surface, JSON.stringify(
20
+ views.map((v) => ({ name: v.name, snapshot: v.snapshot }))
21
+ ));
22
+ } catch {
23
+ }
24
+ }
25
+ export function useSavedViews() {
26
+ return {
27
+ durable: false,
28
+ async list(surface) {
29
+ return readAll(surface);
30
+ },
31
+ async save(surface, name, snapshot) {
32
+ const views = readAll(surface);
33
+ const entry = { id: localId(surface, name), name, snapshot };
34
+ const at = views.findIndex((v) => v.name === name);
35
+ if (at >= 0) views.splice(at, 1, entry);
36
+ else views.push(entry);
37
+ writeAll(surface, views);
38
+ return entry;
39
+ },
40
+ async remove(surface, id) {
41
+ writeAll(surface, readAll(surface).filter((v) => v.id !== id));
42
+ }
43
+ };
44
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The narrowing a list was left in, kept across a page change (PE-608).
3
+ *
4
+ * Leaving a list to open a record — or to check another one — and coming back
5
+ * to an unfiltered table is the small tax every operator pays repeatedly: they
6
+ * re-tick the same facets, re-pick the same sort, and do it again the next
7
+ * time. The state is already a snapshot (it is what a saved view stores), so
8
+ * remembering the last one per surface costs nothing new.
9
+ *
10
+ * ## Why `sessionStorage` and not `localStorage`
11
+ *
12
+ * This is the decision worth arguing, because both are defensible and only one
13
+ * of them is safe by default.
14
+ *
15
+ * A sticky filter is a known way to lose people. An operator opens Orders, sees
16
+ * three rows out of twenty-three thousand, and concludes the data is gone —
17
+ * because the filter that did it was set last Tuesday and nothing on screen
18
+ * looks like an answer to "why is this list short". That is a support ticket,
19
+ * and a scary one.
20
+ *
21
+ * Two things make it safe here rather than merely convenient:
22
+ *
23
+ * 1. **It is visible.** The chip strip under the toolbar names every applied
24
+ * value and the trigger is lit. A restored narrowing announces itself; the
25
+ * version of this feature that is dangerous is the one you cannot see.
26
+ * 2. **It ends with the tab.** `sessionStorage` survives a navigation and a
27
+ * reload — which is what "come back to the list" means — and is gone when
28
+ * the tab closes. Tomorrow morning is a fresh list. `localStorage` would
29
+ * carry Friday's filter into Monday, and `localStorage` is also what the
30
+ * saved views deliberately stopped being: a durable narrowing is a thing
31
+ * you NAME and store on your account, not something a list does to you.
32
+ *
33
+ * So: sticky within a session, explicit across them. Swapping the two is one
34
+ * identifier if that trade ever reads differently.
35
+ *
36
+ * ## What it does not do
37
+ *
38
+ * It does not write the URL. A link to a list is a link to the list, not to how
39
+ * one person last left it — pasting a narrowed URL into a ticket and having a
40
+ * colleague see something else is worse than either behaviour here. The queue
41
+ * pills are the linkable half, and they already carry `?queue=` on purpose.
42
+ */
43
+ export interface StickyListView<T> {
44
+ /**
45
+ * The snapshot this surface was last left in, or `null`.
46
+ *
47
+ * Read once, deliberately: a surface restores on mount and owns its state
48
+ * from then on. A reactive value here would fight the surface for it.
49
+ */
50
+ read: () => T | null;
51
+ /** Remember the current snapshot. Call it whenever the surface's state moves. */
52
+ write: (snapshot: T) => void;
53
+ /** Forget this surface's state — what a "reset" should also do. */
54
+ clear: () => void;
55
+ }
56
+ /**
57
+ * @param key A stable identifier for the surface — the same route pattern the
58
+ * saved views are keyed by, so two lists never share a memory.
59
+ */
60
+ export declare function useStickyListView<T>(key: string): StickyListView<T>;
@@ -0,0 +1,32 @@
1
+ const VERSION = 1;
2
+ export function useStickyListView(key) {
3
+ const storageKey = `commerce.listState.${key}`;
4
+ return {
5
+ read() {
6
+ try {
7
+ if (typeof window === "undefined") return null;
8
+ const raw = window.sessionStorage.getItem(storageKey);
9
+ if (!raw) return null;
10
+ const parsed = JSON.parse(raw);
11
+ if (!parsed || parsed.v !== VERSION) return null;
12
+ return parsed.snapshot ?? null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ },
17
+ write(snapshot) {
18
+ try {
19
+ if (typeof window === "undefined") return;
20
+ window.sessionStorage.setItem(storageKey, JSON.stringify({ v: VERSION, snapshot }));
21
+ } catch {
22
+ }
23
+ },
24
+ clear() {
25
+ try {
26
+ if (typeof window === "undefined") return;
27
+ window.sessionStorage.removeItem(storageKey);
28
+ } catch {
29
+ }
30
+ }
31
+ };
32
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The active tenant, as every tenant-scoped client sends it (PE-828).
3
+ *
4
+ * The contract the Cockpit shares with Console, Baseline and every gateway:
5
+ * each tenant-scoped request carries the active tenant slug in a header, and
6
+ * the service checks it against the token. The header only SELECTS a tenant
7
+ * the token already holds; it never grants one.
8
+ *
9
+ * Before this helper each client had its own copy of "send the header if we
10
+ * happen to know the slug", and most of them sent the request without it while
11
+ * the top-bar selection was still resolving. In an org with two tenants that
12
+ * is an ambiguous request, which the services answer with 400/403/422.
13
+ *
14
+ * So the rule is now one function: wait (bounded) for the active tenant, and
15
+ * refuse to send a tenant-scoped request when there is none. A missing tenant
16
+ * is a `NoActiveTenantError`, which the UI shows as "pick a tenant".
17
+ *
18
+ * Pure (Vue only), so the cockpit, studio-commerce and their tests share it.
19
+ */
20
+ import { type Ref } from 'vue';
21
+ /** The header the platform, Console, Baseline and most gateways read. */
22
+ export declare const TENANT_HEADER = "X-Revenexx-Tenant";
23
+ /** The header the AI gateway, Aria and integrations read instead. */
24
+ export declare const TENANT_ID_HEADER = "X-Tenant-Id";
25
+ /** How long a request waits for the active tenant before it gives up. */
26
+ export declare const TENANT_WAIT_MS = 8000;
27
+ export declare class NoActiveTenantError extends Error {
28
+ readonly status = 400;
29
+ readonly code = "no_active_tenant";
30
+ constructor(message?: string);
31
+ }
32
+ /** True for the error `requireTenant` throws, across bundle copies. */
33
+ export declare function isNoActiveTenant(err: unknown): err is NoActiveTenantError;
34
+ /**
35
+ * Wait for a shared ref to hold a value, bounded (RAD-182).
36
+ *
37
+ * Loads what can be loaded, then waits until the value arrives or `timeoutMs`
38
+ * passes. Resolves null on timeout; never throws.
39
+ */
40
+ export declare function waitForValue<T>(source: Ref<T | null | undefined>, load: () => Promise<unknown>, timeoutMs?: number): Promise<T | null>;
41
+ /** Anything that can name the active tenant: `useConsoleTenants`, `usePlatformTenant`. */
42
+ export interface TenantSource {
43
+ slug: Ref<string | null>;
44
+ load: () => Promise<unknown>;
45
+ }
46
+ /**
47
+ * The active tenant slug, or a `NoActiveTenantError`.
48
+ *
49
+ * Never returns null: a caller that gets past this line has a tenant to send.
50
+ */
51
+ export declare function requireTenant(source: TenantSource, timeoutMs?: number): Promise<string>;
52
+ /**
53
+ * The active tenant from a `usePlatformTenant()` seam, or a `NoActiveTenantError`.
54
+ *
55
+ * The seam's `load` already waits for the selection (the host bounds it), so
56
+ * nothing is waited for twice here.
57
+ */
58
+ export declare function requirePlatformTenant(tenant: TenantSource): Promise<string>;
@@ -0,0 +1,45 @@
1
+ import { watch } from "vue";
2
+ export const TENANT_HEADER = "X-Revenexx-Tenant";
3
+ export const TENANT_ID_HEADER = "X-Tenant-Id";
4
+ export const TENANT_WAIT_MS = 8e3;
5
+ export class NoActiveTenantError extends Error {
6
+ status = 400;
7
+ code = "no_active_tenant";
8
+ constructor(message = "No active tenant. Pick one in the top bar.") {
9
+ super(message);
10
+ this.name = "NoActiveTenantError";
11
+ }
12
+ }
13
+ export function isNoActiveTenant(err) {
14
+ return err?.code === "no_active_tenant";
15
+ }
16
+ export async function waitForValue(source, load, timeoutMs = TENANT_WAIT_MS) {
17
+ if (source.value) return source.value;
18
+ try {
19
+ await load();
20
+ } catch {
21
+ }
22
+ if (source.value) return source.value;
23
+ if (timeoutMs <= 0) return null;
24
+ return await new Promise((resolve) => {
25
+ let stop = null;
26
+ const timer = setTimeout(() => {
27
+ stop?.();
28
+ resolve(null);
29
+ }, timeoutMs);
30
+ stop = watch(source, (value) => {
31
+ if (!value) return;
32
+ clearTimeout(timer);
33
+ stop?.();
34
+ resolve(value);
35
+ });
36
+ });
37
+ }
38
+ export async function requireTenant(source, timeoutMs = TENANT_WAIT_MS) {
39
+ const slug = await waitForValue(source.slug, source.load, timeoutMs);
40
+ if (!slug) throw new NoActiveTenantError();
41
+ return slug;
42
+ }
43
+ export function requirePlatformTenant(tenant) {
44
+ return requireTenant(tenant, 0);
45
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What an app's icon is, read from the one field the manifest documents (PE-560).
3
+ *
4
+ * `manifest.icon` is either a FILE or a SOLAR ICON NAME, told apart by shape:
5
+ *
6
+ * - a file contains a `/` or ends in `.svg`, `.png` or `.webp` — a URL, a
7
+ * `data:` URI, or a path the registry has resolved to one;
8
+ * - a Solar name is kebab-case (`money-bag`), the same token a `cockpit.json`
9
+ * navigation entry carries.
10
+ *
11
+ * Anything else is neither and is skipped. The Cockpit used to read the field
12
+ * three ways — a Solar name in the sidebar, an `<img src>` on the installed
13
+ * pages, not at all in the Marketplace — so no value rendered everywhere, and
14
+ * no app shipped one.
15
+ *
16
+ * Pure on purpose: the Solar registry lookup lives in `solarIcons.ts`, so this
17
+ * module has no imports and is testable without a Nuxt alias.
18
+ */
19
+ export type AppIconSource = {
20
+ kind: 'file';
21
+ src: string;
22
+ } | {
23
+ kind: 'solar';
24
+ name: string;
25
+ };
26
+ /**
27
+ * `money-bag` → `MoneyBag`, the export name in the Solar registry. Snake case
28
+ * (`card_2`) and the export name itself (`Layers`) read the same way.
29
+ * `scripts/icons.mjs` carries a copy (`exportName`) and must agree with it.
30
+ */
31
+ export declare function solarExportName(name: string): string;
32
+ /** How one `manifest.icon` value reads, or null when it is neither shape. */
33
+ export declare function readAppIcon(value: string | null | undefined): AppIconSource | null;
34
+ /**
35
+ * The Solar name of the first `cockpit.json` navigation entry that has one —
36
+ * the glyph the app already shows in its host studio's sidebar, and the
37
+ * fallback for the apps that set no `manifest.icon`.
38
+ */
39
+ export declare function firstNavIcon(navigation: ReadonlyArray<{
40
+ icon?: string | null;
41
+ }> | null | undefined): string | null;
42
+ /**
43
+ * Every source to try, in order: `manifest.icon` (file or Solar name), then the
44
+ * navigation glyph. The renderer walks the list and draws the first that
45
+ * resolves — an image that fails to load, or a name the registry lacks, falls
46
+ * through to the next, and past the end to the generic glyph.
47
+ */
48
+ export declare function appIconCandidates(icon: string | null | undefined, navIcon?: string | null): AppIconSource[];
@@ -0,0 +1,27 @@
1
+ const FILE_EXTENSION = /\.(?:svg|png|webp)$/i;
2
+ const SOLAR_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3
+ export function solarExportName(name) {
4
+ return name.replace(/(^|[-_])([a-z0-9])/gi, (_m, _sep, ch) => ch.toUpperCase());
5
+ }
6
+ export function readAppIcon(value) {
7
+ const v = value?.trim();
8
+ if (!v) return null;
9
+ if (v.includes("/") || FILE_EXTENSION.test(v)) return { kind: "file", src: v };
10
+ if (SOLAR_NAME.test(v)) return { kind: "solar", name: v };
11
+ return null;
12
+ }
13
+ export function firstNavIcon(navigation) {
14
+ for (const item of navigation ?? []) {
15
+ const icon = item.icon?.trim();
16
+ if (icon) return icon;
17
+ }
18
+ return null;
19
+ }
20
+ export function appIconCandidates(icon, navIcon) {
21
+ const out = [];
22
+ const own = readAppIcon(icon);
23
+ if (own) out.push(own);
24
+ const nav = navIcon?.trim();
25
+ if (nav && !(own?.kind === "solar" && own.name === nav)) out.push({ kind: "solar", name: nav });
26
+ return out;
27
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A stable string for any JSON value, for comparing two of them (PE-608).
3
+ *
4
+ * ## The bug this exists for
5
+ *
6
+ * `SavedViewsMenu` decides whether a stored view is the one currently applied
7
+ * by comparing snapshots, and it compared them with `JSON.stringify`. That
8
+ * works exactly as long as both sides were built by the same code in the same
9
+ * order — which stopped being true the moment saved views moved into Postgres.
10
+ *
11
+ * `jsonb` does not preserve key order; it normalises it. So a surface that
12
+ * builds `{ filters, sort }` gets `{ sort, filters }` back from the database,
13
+ * the two strings differ, and the applied view is never recognised as applied.
14
+ * Nothing throws and nothing looks broken — the menu simply never marks
15
+ * anything, and the trigger never shows that a view is in effect. It is the
16
+ * kind of defect that survives review because the code reads as obviously
17
+ * correct.
18
+ *
19
+ * ## What "canonical" means here
20
+ *
21
+ * Object keys sorted, recursively. Arrays left alone, because their order IS
22
+ * their meaning — `["a","b"]` and `["b","a"]` are different filter values as
23
+ * far as a query is concerned, and a facet that wants them treated as a set
24
+ * sorts them before it stores them.
25
+ *
26
+ * `undefined` is dropped from objects, exactly as `JSON.stringify` does, so an
27
+ * absent key and an explicitly-undefined one compare equal. That matters:
28
+ * `{ queue: undefined }` is what a spread produces for a surface with no queue,
29
+ * and the server round-trips it as an absent key.
30
+ */
31
+ /**
32
+ * Deterministic JSON for `value` — same input, same string, whatever order the
33
+ * keys arrived in.
34
+ *
35
+ * A cycle would make this recurse forever, so it is caught and reported as a
36
+ * sentinel rather than thrown: the one caller is a comparison in a computed,
37
+ * and a snapshot is not worth crashing a toolbar over. Two cyclic values
38
+ * compare equal, which is wrong but harmless — and a snapshot with a cycle
39
+ * could never have been stored in the first place.
40
+ */
41
+ export declare function canonicalJson(value: unknown): string;
42
+ /** Do two JSON values mean the same thing, whatever order their keys are in? */
43
+ export declare function sameJson(a: unknown, b: unknown): boolean;
@@ -0,0 +1,38 @@
1
+ export function canonicalJson(value) {
2
+ const seen = /* @__PURE__ */ new WeakSet();
3
+ const walk = (input) => {
4
+ if (input === null) return "null";
5
+ const type = typeof input;
6
+ if (type === "number") return Number.isFinite(input) ? String(input) : "null";
7
+ if (type === "boolean") return String(input);
8
+ if (type === "string") return JSON.stringify(input);
9
+ if (type === "undefined" || type === "function" || type === "symbol") return "undefined";
10
+ if (type === "bigint") return JSON.stringify(String(input));
11
+ const object = input;
12
+ if (seen.has(object)) return '"[cycle]"';
13
+ seen.add(object);
14
+ try {
15
+ if (Array.isArray(object)) {
16
+ return `[${object.map((item) => {
17
+ const part = walk(item);
18
+ return part === "undefined" ? "null" : part;
19
+ }).join(",")}]`;
20
+ }
21
+ if (object instanceof Date) return JSON.stringify(object.toISOString());
22
+ const parts = [];
23
+ for (const key of Object.keys(object).sort()) {
24
+ const part = walk(object[key]);
25
+ if (part === "undefined") continue;
26
+ parts.push(`${JSON.stringify(key)}:${part}`);
27
+ }
28
+ return `{${parts.join(",")}}`;
29
+ } finally {
30
+ seen.delete(object);
31
+ }
32
+ };
33
+ const out = walk(value);
34
+ return out === "undefined" ? "null" : out;
35
+ }
36
+ export function sameJson(a, b) {
37
+ return canonicalJson(a) === canonicalJson(b);
38
+ }
@@ -0,0 +1,4 @@
1
+ export declare const DASHBOARD_ICONS: Record<string, unknown>;
2
+ /** Every name the picker may offer, in a stable order. */
3
+ export declare const PICKABLE_ICONS: string[];
4
+ export declare function dashboardIcon(name?: string): unknown;
@@ -0,0 +1,50 @@
1
+ import {
2
+ Bag,
3
+ BillList,
4
+ Bolt,
5
+ Box,
6
+ Card,
7
+ Cart,
8
+ ChartSquare,
9
+ ClipboardList,
10
+ DangerTriangle,
11
+ Earth,
12
+ GraphUp,
13
+ Hashtag,
14
+ Home,
15
+ Layers,
16
+ List,
17
+ MagicStick,
18
+ NotebookMinimalistic,
19
+ Pulse,
20
+ Routing,
21
+ UsersGroupRounded,
22
+ Widget
23
+ } from "#solar-icons";
24
+ export const DASHBOARD_ICONS = {
25
+ Bag,
26
+ BillList,
27
+ Bolt,
28
+ Box,
29
+ Card,
30
+ Cart,
31
+ ChartSquare,
32
+ ClipboardList,
33
+ DangerTriangle,
34
+ Earth,
35
+ GraphUp,
36
+ Hashtag,
37
+ Home,
38
+ Layers,
39
+ List,
40
+ MagicStick,
41
+ NotebookMinimalistic,
42
+ Pulse,
43
+ Routing,
44
+ UsersGroupRounded,
45
+ Widget
46
+ };
47
+ export const PICKABLE_ICONS = Object.keys(DASHBOARD_ICONS);
48
+ export function dashboardIcon(name) {
49
+ return name && DASHBOARD_ICONS[name] || Widget;
50
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The parts of `DetailSheet` that are arithmetic rather than UI (PE-691).
3
+ *
4
+ * Kept out of the component and the composable so they can be tested without a
5
+ * router or a DOM: which record is next, what the query looks like with the
6
+ * panel open or shut, and whether a keypress belongs to the panel or to the
7
+ * control it landed on.
8
+ */
9
+ export type QueryValue = string | null | (string | null)[] | undefined;
10
+ export type Query = Record<string, QueryValue>;
11
+ /** The open record id from a query value — the first one if a link repeats it. */
12
+ export declare function idFromQuery(value: QueryValue): string | null;
13
+ /** `query` with `key` set to `id`, or without `key` when `id` is null. */
14
+ export declare function queryWith(query: Query, key: string, id: string | null): Query;
15
+ /**
16
+ * The id `step` places away from `current` in `ids`, or null at either end.
17
+ *
18
+ * No wrap-around: arrowing past the last row and landing on the first reads as
19
+ * the list having jumped, and in a paginated list the first row is not even on
20
+ * the page. An id that is no longer in the list (deleted, filtered out) has no
21
+ * neighbours either — guessing one would open a record nobody pointed at.
22
+ */
23
+ export declare function neighbourId(ids: readonly string[], current: string | null, step: -1 | 1): string | null;
24
+ interface KeyEvent {
25
+ key: string;
26
+ altKey?: boolean;
27
+ ctrlKey?: boolean;
28
+ metaKey?: boolean;
29
+ shiftKey?: boolean;
30
+ defaultPrevented?: boolean;
31
+ target?: unknown;
32
+ }
33
+ /**
34
+ * Whether the focused element handles arrows and Escape itself: a text field
35
+ * (caret, clearing), a select, a menu, a listbox, a slider. The panel's keys
36
+ * give way there — taking them would break the control to save one click.
37
+ */
38
+ export declare function targetOwnsKeys(target: unknown): boolean;
39
+ /** Whether ArrowUp/ArrowDown should move to the neighbouring record, and which way. */
40
+ export declare function isRecordStepKey(ev: KeyEvent): -1 | 1 | null;
41
+ /** Whether Escape should close the panel rather than the control it landed on. */
42
+ export declare function isCloseKey(ev: KeyEvent): boolean;
43
+ export {};
@@ -0,0 +1,34 @@
1
+ export function idFromQuery(value) {
2
+ const v = Array.isArray(value) ? value[0] : value;
3
+ return typeof v === "string" && v !== "" ? v : null;
4
+ }
5
+ export function queryWith(query, key, id) {
6
+ const next = { ...query };
7
+ if (id === null) delete next[key];
8
+ else next[key] = id;
9
+ return next;
10
+ }
11
+ export function neighbourId(ids, current, step) {
12
+ if (current === null) return null;
13
+ const i = ids.indexOf(current);
14
+ if (i < 0) return null;
15
+ return ids[i + step] ?? null;
16
+ }
17
+ export function targetOwnsKeys(target) {
18
+ const t = target;
19
+ if (!t) return false;
20
+ const tag = (t.tagName ?? "").toUpperCase();
21
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
22
+ if (t.isContentEditable) return true;
23
+ return !!t.closest?.('[role="menu"],[role="listbox"],[role="combobox"],[role="slider"],[role="spinbutton"],[role="radiogroup"],[role="tablist"]');
24
+ }
25
+ export function isRecordStepKey(ev) {
26
+ if (ev.defaultPrevented) return null;
27
+ if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) return null;
28
+ const step = ev.key === "ArrowDown" ? 1 : ev.key === "ArrowUp" ? -1 : null;
29
+ if (step === null) return null;
30
+ return targetOwnsKeys(ev.target) ? null : step;
31
+ }
32
+ export function isCloseKey(ev) {
33
+ return ev.key === "Escape" && !ev.defaultPrevented && !targetOwnsKeys(ev.target);
34
+ }