@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.
- package/dist/module.d.mts +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -1
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.d.vue.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +39 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue.d.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardControls.d.vue.ts +58 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue +209 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue.d.ts +58 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.d.vue.ts +17 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue +87 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue.d.ts +17 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.d.vue.ts +19 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +61 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue.d.ts +19 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.d.vue.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +100 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue.d.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.d.vue.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +155 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue.d.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.d.vue.ts +64 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue +79 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue.d.ts +64 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.d.vue.ts +44 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +153 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue.d.ts +44 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +80 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.d.vue.ts +7 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue +30 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue.d.ts +7 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.d.vue.ts +9 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue +13 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue.d.ts +9 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.d.vue.ts +24 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue +140 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue.d.ts +24 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.d.vue.ts +23 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue +124 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue.d.ts +23 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -1
- package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -1
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +105 -28
- package/dist/runtime/components/DataTable/DataTable.vue +403 -38
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +105 -28
- package/dist/runtime/components/DataTable/FacetChips.d.vue.ts +17 -0
- package/dist/runtime/components/DataTable/FacetChips.vue +72 -0
- package/dist/runtime/components/DataTable/FacetChips.vue.d.ts +17 -0
- package/dist/runtime/components/DataTable/FacetPanel.d.vue.ts +21 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue +359 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue.d.ts +21 -0
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +6 -5
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +6 -5
- package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -1
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue +83 -63
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -1
- package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +9 -3
- package/dist/runtime/components/DataTable/SortMenu.vue +40 -12
- package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +9 -3
- package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +4 -2
- package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +4 -2
- package/dist/runtime/components/Form/FieldCell.d.vue.ts +32 -0
- package/dist/runtime/components/Form/FieldCell.vue +46 -0
- package/dist/runtime/components/Form/FieldCell.vue.d.ts +32 -0
- package/dist/runtime/components/Form/FieldGrid.d.vue.ts +13 -0
- package/dist/runtime/components/Form/FieldGrid.vue +11 -0
- package/dist/runtime/components/Form/FieldGrid.vue.d.ts +13 -0
- package/dist/runtime/components/Form/FormContextCard.d.vue.ts +33 -0
- package/dist/runtime/components/Form/FormContextCard.vue +56 -0
- package/dist/runtime/components/Form/FormContextCard.vue.d.ts +33 -0
- package/dist/runtime/components/Form/FormLayout.d.vue.ts +47 -0
- package/dist/runtime/components/Form/FormLayout.vue +169 -0
- package/dist/runtime/components/Form/FormLayout.vue.d.ts +47 -0
- package/dist/runtime/components/Form/FormSection.d.vue.ts +25 -0
- package/dist/runtime/components/Form/FormSection.vue +48 -0
- package/dist/runtime/components/Form/FormSection.vue.d.ts +25 -0
- package/dist/runtime/components/Form/SaveBar.d.vue.ts +44 -0
- package/dist/runtime/components/Form/SaveBar.vue +71 -0
- package/dist/runtime/components/Form/SaveBar.vue.d.ts +44 -0
- package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +2 -1
- package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +2 -1
- package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +4 -3
- package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +4 -3
- package/dist/runtime/components/Media/MediaUploader.d.vue.ts +4 -3
- package/dist/runtime/components/Media/MediaUploader.vue.d.ts +4 -3
- package/dist/runtime/components/Shared/AppIcon.d.vue.ts +21 -0
- package/dist/runtime/components/Shared/AppIcon.vue +47 -0
- package/dist/runtime/components/Shared/AppIcon.vue.d.ts +21 -0
- package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/DetailSheet.d.vue.ts +60 -0
- package/dist/runtime/components/Shared/DetailSheet.vue +207 -0
- package/dist/runtime/components/Shared/DetailSheet.vue.d.ts +60 -0
- package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +7 -6
- package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +7 -6
- package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/KpiTile.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/KpiTile.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/ListSurface.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/ListSurface.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/PageHeader.d.vue.ts +7 -6
- package/dist/runtime/components/Shared/PageHeader.vue.d.ts +7 -6
- package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +8 -7
- package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +8 -7
- package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/SortHeader.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/SortHeader.vue +6 -2
- package/dist/runtime/components/Shared/SortHeader.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/StateCard.d.vue.ts +6 -5
- package/dist/runtime/components/Shared/StateCard.vue.d.ts +6 -5
- package/dist/runtime/components/Shared/StudioCard.d.vue.ts +5 -4
- package/dist/runtime/components/Shared/StudioCard.vue.d.ts +5 -4
- package/dist/runtime/components/Shared/TabBar.d.vue.ts +9 -10
- package/dist/runtime/components/Shared/TabBar.vue +26 -7
- package/dist/runtime/components/Shared/TabBar.vue.d.ts +9 -10
- package/dist/runtime/components/Shared/ViewTab.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/ViewTab.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/Wizard.d.vue.ts +10 -7
- package/dist/runtime/components/Shared/Wizard.vue.d.ts +10 -7
- package/dist/runtime/composables/useDashboards.d.ts +27 -0
- package/dist/runtime/composables/useDashboards.js +143 -0
- package/dist/runtime/composables/useDetailSheet.d.ts +20 -0
- package/dist/runtime/composables/useDetailSheet.js +50 -0
- package/dist/runtime/composables/usePlatformTenant.d.ts +6 -0
- package/dist/runtime/composables/useSavedViews.d.ts +44 -0
- package/dist/runtime/composables/useSavedViews.js +44 -0
- package/dist/runtime/composables/useStickyListView.d.ts +60 -0
- package/dist/runtime/composables/useStickyListView.js +32 -0
- package/dist/runtime/lib/activeTenant.d.ts +58 -0
- package/dist/runtime/lib/activeTenant.js +45 -0
- package/dist/runtime/lib/appIcon.d.ts +48 -0
- package/dist/runtime/lib/appIcon.js +27 -0
- package/dist/runtime/lib/canonicalJson.d.ts +43 -0
- package/dist/runtime/lib/canonicalJson.js +38 -0
- package/dist/runtime/lib/dashboardIcons.d.ts +4 -0
- package/dist/runtime/lib/dashboardIcons.js +50 -0
- package/dist/runtime/lib/detailSheet.d.ts +43 -0
- package/dist/runtime/lib/detailSheet.js +34 -0
- package/dist/runtime/lib/facets.d.ts +200 -0
- package/dist/runtime/lib/facets.js +171 -0
- package/dist/runtime/lib/formLayout.d.ts +94 -0
- package/dist/runtime/lib/formLayout.js +70 -0
- package/dist/runtime/lib/formLayoutContext.d.ts +26 -0
- package/dist/runtime/lib/formLayoutContext.js +1 -0
- package/dist/runtime/lib/gridTracks.d.ts +55 -0
- package/dist/runtime/lib/gridTracks.js +29 -0
- package/dist/runtime/lib/rowMove.d.ts +38 -0
- package/dist/runtime/lib/rowMove.js +20 -0
- package/dist/runtime/lib/solarIconMap.d.ts +15 -0
- package/dist/runtime/lib/solarIconMap.js +280 -0
- package/dist/runtime/lib/solarIcons.d.ts +37 -0
- package/dist/runtime/lib/solarIcons.js +25 -0
- package/dist/runtime/lib/widgetRegistry.d.ts +124 -0
- package/dist/runtime/lib/widgetRegistry.js +66 -0
- package/dist/runtime/types/dashboard.d.ts +95 -0
- package/dist/runtime/types/dashboard.js +2 -0
- package/dist/types.d.mts +12 -0
- 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,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
|
+
}
|