@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.
- package/dist/module.d.mts +7 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +37 -1
- package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.vue +93 -36
- package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -0
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +53 -4
- package/dist/runtime/components/DataTable/DataTable.vue +274 -118
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +53 -4
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +14 -3
- package/dist/runtime/components/DataTable/FilterMenu.vue +35 -29
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +14 -3
- package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -0
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue +189 -0
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -0
- package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +13 -0
- package/dist/runtime/components/DataTable/SortMenu.vue +47 -0
- package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +13 -0
- package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +20 -0
- package/dist/runtime/components/DataTable/ViewModeSwitch.vue +26 -0
- package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +20 -0
- package/dist/runtime/components/DataTable/types.d.ts +49 -2
- package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +17 -0
- package/dist/runtime/components/Media/MediaFolderTree.vue +65 -0
- package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +17 -0
- package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Media/MediaPickerDialog.vue +163 -0
- package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Media/MediaUploader.d.vue.ts +29 -0
- package/dist/runtime/components/Media/MediaUploader.vue +161 -0
- package/dist/runtime/components/Media/MediaUploader.vue.d.ts +29 -0
- package/dist/runtime/components/Shared/BuildLogsFlyout.vue +14 -13
- package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +55 -0
- package/dist/runtime/components/Shared/FloatingPanel.vue +118 -0
- package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +55 -0
- package/dist/runtime/components/Shared/GitDeploymentSettings.vue +11 -10
- package/dist/runtime/components/Shared/ListSurface.d.vue.ts +38 -0
- package/dist/runtime/components/Shared/ListSurface.vue +27 -0
- package/dist/runtime/components/Shared/ListSurface.vue.d.ts +38 -0
- package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +52 -0
- package/dist/runtime/components/Shared/RecordLayout.vue +63 -0
- package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +52 -0
- package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +26 -0
- package/dist/runtime/components/Shared/RecordRailCard.vue +20 -0
- package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +26 -0
- package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +5 -0
- package/dist/runtime/components/Shared/SidebarNav.vue +6 -26
- package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +5 -0
- package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +15 -0
- package/dist/runtime/components/Shared/SidebarNavLink.vue +49 -0
- package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +15 -0
- package/dist/runtime/components/Shared/Wizard.vue +6 -3
- package/dist/runtime/composables/useFloatingPanel.d.ts +69 -0
- package/dist/runtime/composables/useFloatingPanel.js +112 -0
- package/dist/runtime/composables/useListRail.d.ts +9 -0
- package/dist/runtime/composables/useListRail.js +19 -0
- package/dist/runtime/composables/useMediaLibrary.d.ts +41 -0
- package/dist/runtime/composables/useMediaLibrary.js +36 -0
- package/dist/runtime/composables/usePlatformAuth.d.ts +18 -0
- package/dist/runtime/composables/usePlatformTalkback.d.ts +29 -0
- package/dist/runtime/composables/usePlatformTalkback.js +54 -0
- package/dist/runtime/composables/useT.d.ts +11 -0
- package/dist/runtime/composables/useT.js +8 -0
- package/dist/runtime/composables/useTalkbackChannels.d.ts +93 -0
- package/dist/runtime/composables/useTalkbackChannels.js +56 -0
- package/dist/runtime/lib/columnOrder.d.ts +45 -0
- package/dist/runtime/lib/columnOrder.js +28 -0
- package/dist/runtime/lib/columnVisibility.d.ts +41 -0
- package/dist/runtime/lib/columnVisibility.js +12 -0
- package/dist/runtime/lib/dataTableFilters.d.ts +83 -0
- package/dist/runtime/lib/dataTableFilters.js +23 -0
- package/dist/runtime/lib/formatBridge.d.ts +50 -0
- package/dist/runtime/lib/formatBridge.js +54 -0
- package/dist/runtime/lib/i18nBridge.d.ts +36 -0
- package/dist/runtime/lib/i18nBridge.js +17 -0
- package/dist/runtime/lib/isoLists.d.ts +61 -0
- package/dist/runtime/lib/isoLists.js +62 -0
- package/dist/runtime/lib/sidebarNav.d.ts +34 -0
- package/dist/runtime/lib/sidebarNav.js +22 -0
- package/dist/runtime/lib/talkback.d.ts +59 -0
- package/dist/runtime/lib/talkback.js +49 -0
- package/dist/runtime/types/nav.d.ts +15 -1
- package/dist/runtime/types/storage.d.ts +67 -0
- package/dist/runtime/types/storage.js +0 -0
- package/dist/types.d.mts +14 -0
- package/package.json +19 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of a DataTable's columns are drawn.
|
|
3
|
+
*
|
|
4
|
+
* Pulled out of `DataTable.vue` because the rule has a trap in it that a
|
|
5
|
+
* component test would not have caught cheaply, and that shipped once already.
|
|
6
|
+
*
|
|
7
|
+
* A table's column list is NOT fixed for the life of the component. A tab that
|
|
8
|
+
* only offers Markets once the service proves it has the column, or a validity
|
|
9
|
+
* window once the deployment has grown one, hands the table a LONGER array on a
|
|
10
|
+
* later render than the one it was set up with.
|
|
11
|
+
*
|
|
12
|
+
* The table used to seed a set of visible keys once and filter by membership.
|
|
13
|
+
* Every column that turned up afterwards was therefore missing from that set and
|
|
14
|
+
* arrived SWITCHED OFF — present in the picker, unticked, and invisible to
|
|
15
|
+
* everybody who did not go looking. Nobody reports that: from the outside it
|
|
16
|
+
* looks like the column was never built.
|
|
17
|
+
*
|
|
18
|
+
* So what is remembered is the user's own decisions — the columns they ticked
|
|
19
|
+
* away — and everything else follows its declared default. A column that
|
|
20
|
+
* appears later gets that default, which is what somebody adding a column
|
|
21
|
+
* expects and what the previous rule quietly denied them.
|
|
22
|
+
*/
|
|
23
|
+
export interface VisibilityColumn {
|
|
24
|
+
key: string;
|
|
25
|
+
/** `false` means "start hidden". Absent means shown. */
|
|
26
|
+
visible?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The columns hidden before the user has touched anything — the declared
|
|
30
|
+
* defaults, and nothing else.
|
|
31
|
+
*/
|
|
32
|
+
export declare function initiallyHidden(columns: readonly VisibilityColumn[]): Set<string>;
|
|
33
|
+
/**
|
|
34
|
+
* The columns to draw: everything except what the user put away.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately keyed on hidden rather than on visible. The set of things a
|
|
37
|
+
* person has decided against is stable; the set of things that exist is not.
|
|
38
|
+
*/
|
|
39
|
+
export declare function visibleColumnsOf<T extends VisibilityColumn>(columns: readonly T[], hidden: ReadonlySet<string>): T[];
|
|
40
|
+
/** Add or remove one column from the user's hidden set. */
|
|
41
|
+
export declare function toggleHidden(hidden: ReadonlySet<string>, key: string): Set<string>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function initiallyHidden(columns) {
|
|
2
|
+
return new Set(columns.filter((c) => c.visible === false).map((c) => c.key));
|
|
3
|
+
}
|
|
4
|
+
export function visibleColumnsOf(columns, hidden) {
|
|
5
|
+
return columns.filter((c) => !hidden.has(c.key));
|
|
6
|
+
}
|
|
7
|
+
export function toggleHidden(hidden, key) {
|
|
8
|
+
const next = new Set(hidden);
|
|
9
|
+
if (next.has(key)) next.delete(key);
|
|
10
|
+
else next.add(key);
|
|
11
|
+
return next;
|
|
12
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a row's value is for a filter dimension — and why it can be several.
|
|
3
|
+
*
|
|
4
|
+
* Pulled out of `DataTable.vue` because the rule underneath it changed shape,
|
|
5
|
+
* and the change is the kind that reads as harmless and is not.
|
|
6
|
+
*
|
|
7
|
+
* ## A row used to belong to exactly one bucket
|
|
8
|
+
*
|
|
9
|
+
* `getter: (row) => string` gives every row ONE value per dimension. For status
|
|
10
|
+
* or channel that is the truth. For anything a row can have several of — the
|
|
11
|
+
* languages a template is written in, the markets it is scoped to, the tags on
|
|
12
|
+
* a product — it is not, and the only way to express it was to make the SET the
|
|
13
|
+
* value:
|
|
14
|
+
*
|
|
15
|
+
* getter: r => r.locales.join(', ')
|
|
16
|
+
*
|
|
17
|
+
* which produces a filter menu that reads
|
|
18
|
+
*
|
|
19
|
+
* Deutsch (Deutschland) 2
|
|
20
|
+
* Deutsch (Deutschland) + British English 19
|
|
21
|
+
* Deutsch (Deutschland) + British English + polski (Polska) 2
|
|
22
|
+
* Deutsch (Deutschland) + British English + français + polski 1
|
|
23
|
+
* …
|
|
24
|
+
*
|
|
25
|
+
* Two things are wrong with that and only one of them is cosmetic.
|
|
26
|
+
*
|
|
27
|
+
* The visible one: the options are COMBINATIONS, so the list grows with the
|
|
28
|
+
* powerset of the values. Four languages produce up to fifteen entries, six up
|
|
29
|
+
* to sixty-three. No menu, panel or layout survives that.
|
|
30
|
+
*
|
|
31
|
+
* The one that matters: "which of these have Polish?" cannot be asked. The
|
|
32
|
+
* answer is spread across three options, and none of the counts is it. An
|
|
33
|
+
* operator who picks the option that looks closest gets a confident, wrong
|
|
34
|
+
* number — the counts adding up to exactly the row total is the tell that this
|
|
35
|
+
* is a partition rather than a set of overlapping questions.
|
|
36
|
+
*
|
|
37
|
+
* ## So a getter may now return an array
|
|
38
|
+
*
|
|
39
|
+
* `string | string[] | null | undefined`. A row lands in as many buckets as it
|
|
40
|
+
* has values, the options become the four languages rather than the fifteen
|
|
41
|
+
* combinations, and "polski 6" means what it says.
|
|
42
|
+
*
|
|
43
|
+
* ## The string path is byte-for-byte what it was
|
|
44
|
+
*
|
|
45
|
+
* Deliberately: every existing surface passes a string getter and none of them
|
|
46
|
+
* should move a pixel. A string — INCLUDING the empty string — is one value,
|
|
47
|
+
* and `null`/`undefined` still collapse to `''` and count under that key, which
|
|
48
|
+
* is what the old `String(getter(row) ?? '')` did. Only the array path is new.
|
|
49
|
+
*
|
|
50
|
+
* Inside an array, empty entries ARE dropped: a template with no languages
|
|
51
|
+
* belongs to no language, and letting it pile into a `''` bucket would invent a
|
|
52
|
+
* silent option nobody declared.
|
|
53
|
+
*
|
|
54
|
+
* ## The counts stop summing to the row total, on purpose
|
|
55
|
+
*
|
|
56
|
+
* For a multi-valued dimension they now overlap, because a row with three
|
|
57
|
+
* languages is counted under three of them. That looks like a bug to anybody
|
|
58
|
+
* who checks the arithmetic, and correcting it would restore exactly the
|
|
59
|
+
* defect this exists to remove.
|
|
60
|
+
*/
|
|
61
|
+
export type FilterGetter<T> = (row: T) => string | string[] | null | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The buckets one row belongs to for one dimension.
|
|
64
|
+
*
|
|
65
|
+
* Always an array, so the caller never has to branch on which shape the surface
|
|
66
|
+
* happened to hand over.
|
|
67
|
+
*/
|
|
68
|
+
export declare function filterValues<T>(getter: FilterGetter<T>, row: T): string[];
|
|
69
|
+
/** How many rows sit in each bucket. Overlapping where a row has several. */
|
|
70
|
+
export declare function countByValue<T>(rows: readonly T[], getter: FilterGetter<T>): Record<string, number>;
|
|
71
|
+
/**
|
|
72
|
+
* Does a row survive one dimension's active selection?
|
|
73
|
+
*
|
|
74
|
+
* `active` may be one value or SEVERAL, because the filter panel that replaced
|
|
75
|
+
* the old cascading menu lets a reader tick more than one option in a facet.
|
|
76
|
+
* Several means ANY of them — "Active or Draft", not "Active and Draft", which
|
|
77
|
+
* is the only reading that can ever match anything: a row has one status.
|
|
78
|
+
*
|
|
79
|
+
* Not filtering is `null`, `undefined` or an EMPTY array, and none of those is
|
|
80
|
+
* the same as filtering on nothing. "All" is the absence of a choice, and a row
|
|
81
|
+
* whose value happens to be `''` must not be the only thing that survives it.
|
|
82
|
+
*/
|
|
83
|
+
export declare function matchesFilter<T>(getter: FilterGetter<T>, row: T, active: string | readonly string[] | null | undefined): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function filterValues(getter, row) {
|
|
2
|
+
const raw = getter(row);
|
|
3
|
+
if (Array.isArray(raw)) {
|
|
4
|
+
return raw.map((v) => String(v ?? "")).filter((v) => v !== "");
|
|
5
|
+
}
|
|
6
|
+
return [String(raw ?? "")];
|
|
7
|
+
}
|
|
8
|
+
export function countByValue(rows, getter) {
|
|
9
|
+
const counts = {};
|
|
10
|
+
for (const row of rows) {
|
|
11
|
+
for (const value of filterValues(getter, row)) {
|
|
12
|
+
counts[value] = (counts[value] ?? 0) + 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return counts;
|
|
16
|
+
}
|
|
17
|
+
export function matchesFilter(getter, row, active) {
|
|
18
|
+
if (active == null) return true;
|
|
19
|
+
const wanted = typeof active === "string" ? [active] : active;
|
|
20
|
+
if (!wanted.length) return true;
|
|
21
|
+
const values = filterValues(getter, row);
|
|
22
|
+
return wanted.some((v) => values.includes(v));
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* What the host registers: both reactive, so a computed that formatted once
|
|
4
|
+
* re-runs when the operator changes the preference — no reload.
|
|
5
|
+
*/
|
|
6
|
+
export interface FormatPreferenceSource {
|
|
7
|
+
/** Resolved BCP-47 tag, e.g. `de-AT`. Never `'auto'` — the host resolves. */
|
|
8
|
+
locale: Ref<string>;
|
|
9
|
+
/**
|
|
10
|
+
* `true` forces a 12-hour clock, `false` a 24-hour one, `undefined` leaves
|
|
11
|
+
* the choice to the locale's own convention (which is what `Intl` does when
|
|
12
|
+
* the option is absent).
|
|
13
|
+
*/
|
|
14
|
+
hour12: Ref<boolean | undefined>;
|
|
15
|
+
}
|
|
16
|
+
/** Used when there is no host AND no i18n bridge. See the file comment. */
|
|
17
|
+
export declare const FALLBACK_FORMAT_LOCALE = "en-GB";
|
|
18
|
+
/**
|
|
19
|
+
* The region an `auto` preference resolves to for a UI language.
|
|
20
|
+
*
|
|
21
|
+
* Takes the language subtag, so `en-US` and `en` land on the same answer, and
|
|
22
|
+
* falls back to {@link FALLBACK_FORMAT_LOCALE} for a language we do not ship.
|
|
23
|
+
*/
|
|
24
|
+
export declare function defaultRegionForLanguage(language: string | null | undefined): string;
|
|
25
|
+
/** Called once by the host. Pass `null` to detach (tests). */
|
|
26
|
+
export declare function setFormatPreferences(source: FormatPreferenceSource | null): void;
|
|
27
|
+
/** Whether a host preference is installed. Rarely needed — prefer the getters. */
|
|
28
|
+
export declare function hasFormatPreferences(): boolean;
|
|
29
|
+
/** The BCP-47 tag every package should format in. */
|
|
30
|
+
export declare function formatLocale(): string;
|
|
31
|
+
/** The operator's clock, or `undefined` to let the locale decide. */
|
|
32
|
+
export declare function hour12(): boolean | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* A cached `DateTimeFormat` for an explicitly given locale and clock.
|
|
35
|
+
*
|
|
36
|
+
* The host uses this one: it has already resolved both from its own reactive
|
|
37
|
+
* state and must not have the answer re-derived from a registry it may not
|
|
38
|
+
* have populated yet. Packages want {@link dateTimeFormat}.
|
|
39
|
+
*
|
|
40
|
+
* `clock` is folded into the options rather than left to each call site, but an
|
|
41
|
+
* explicit `hour12` in `options` still wins — a cell that must print a 24-hour
|
|
42
|
+
* timestamp regardless (a log line, a cron preview) can say so.
|
|
43
|
+
*/
|
|
44
|
+
export declare function dateTimeFormatFor(locale: string, options?: Intl.DateTimeFormatOptions, clock?: boolean): Intl.DateTimeFormat;
|
|
45
|
+
/** A cached `NumberFormat` for an explicitly given locale. */
|
|
46
|
+
export declare function numberFormatFor(locale: string, options?: Intl.NumberFormatOptions): Intl.NumberFormat;
|
|
47
|
+
/** A `DateTimeFormat` for whatever the host registered. What packages call. */
|
|
48
|
+
export declare function dateTimeFormat(options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
|
|
49
|
+
/** A `NumberFormat` for whatever the host registered. What packages call. */
|
|
50
|
+
export declare function numberFormat(options?: Intl.NumberFormatOptions): Intl.NumberFormat;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { shallowRef } from "vue";
|
|
2
|
+
import { currentLocale } from "./i18nBridge.js";
|
|
3
|
+
export const FALLBACK_FORMAT_LOCALE = "en-GB";
|
|
4
|
+
const DEFAULT_REGION_BY_LANGUAGE = {
|
|
5
|
+
de: "de-DE",
|
|
6
|
+
en: "en-GB"
|
|
7
|
+
};
|
|
8
|
+
export function defaultRegionForLanguage(language) {
|
|
9
|
+
if (!language) return FALLBACK_FORMAT_LOCALE;
|
|
10
|
+
const subtag = String(language).split(/[-_]/)[0].toLowerCase();
|
|
11
|
+
return DEFAULT_REGION_BY_LANGUAGE[subtag] ?? FALLBACK_FORMAT_LOCALE;
|
|
12
|
+
}
|
|
13
|
+
const registered = shallowRef(null);
|
|
14
|
+
export function setFormatPreferences(source) {
|
|
15
|
+
registered.value = source;
|
|
16
|
+
}
|
|
17
|
+
export function hasFormatPreferences() {
|
|
18
|
+
return registered.value !== null;
|
|
19
|
+
}
|
|
20
|
+
export function formatLocale() {
|
|
21
|
+
const source = registered.value;
|
|
22
|
+
if (source) return source.locale.value;
|
|
23
|
+
return defaultRegionForLanguage(currentLocale());
|
|
24
|
+
}
|
|
25
|
+
export function hour12() {
|
|
26
|
+
return registered.value?.hour12.value;
|
|
27
|
+
}
|
|
28
|
+
const dateTimeFormats = /* @__PURE__ */ new Map();
|
|
29
|
+
const numberFormats = /* @__PURE__ */ new Map();
|
|
30
|
+
export function dateTimeFormatFor(locale, options = {}, clock) {
|
|
31
|
+
const resolved = clock === void 0 || "hour12" in options ? options : { ...options, hour12: clock };
|
|
32
|
+
const key = `${locale}|${JSON.stringify(resolved)}`;
|
|
33
|
+
let format = dateTimeFormats.get(key);
|
|
34
|
+
if (!format) {
|
|
35
|
+
format = new Intl.DateTimeFormat(locale, resolved);
|
|
36
|
+
dateTimeFormats.set(key, format);
|
|
37
|
+
}
|
|
38
|
+
return format;
|
|
39
|
+
}
|
|
40
|
+
export function numberFormatFor(locale, options = {}) {
|
|
41
|
+
const key = `${locale}|${JSON.stringify(options)}`;
|
|
42
|
+
let format = numberFormats.get(key);
|
|
43
|
+
if (!format) {
|
|
44
|
+
format = new Intl.NumberFormat(locale, options);
|
|
45
|
+
numberFormats.set(key, format);
|
|
46
|
+
}
|
|
47
|
+
return format;
|
|
48
|
+
}
|
|
49
|
+
export function dateTimeFormat(options = {}) {
|
|
50
|
+
return dateTimeFormatFor(formatLocale(), options, hour12());
|
|
51
|
+
}
|
|
52
|
+
export function numberFormat(options = {}) {
|
|
53
|
+
return numberFormatFor(formatLocale(), options);
|
|
54
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
/** Interpolation values for a message with `{placeholders}`. */
|
|
3
|
+
export type TranslateNamed = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* The subset of vue-i18n's `t` the packages are allowed to rely on:
|
|
6
|
+
* `t(key)`, `t(key, named)`, `t(key, plural)`, `t(key, named, plural)`.
|
|
7
|
+
*
|
|
8
|
+
* The last one is the order vue-i18n takes for a plural message that also has
|
|
9
|
+
* placeholders. Its `(key, plural, third)` overload reads the third argument as
|
|
10
|
+
* `TranslateOptions`, so a bare named bag passed there interpolates nothing —
|
|
11
|
+
* hence only the named-first form is offered here.
|
|
12
|
+
*/
|
|
13
|
+
export interface Translate {
|
|
14
|
+
(key: string): string;
|
|
15
|
+
(key: string, named: TranslateNamed): string;
|
|
16
|
+
(key: string, plural: number): string;
|
|
17
|
+
(key: string, named: TranslateNamed, plural: number): string;
|
|
18
|
+
}
|
|
19
|
+
export interface Translator {
|
|
20
|
+
t: Translate;
|
|
21
|
+
/** The host's current UI locale. Reactive, so computeds re-run on a switch. */
|
|
22
|
+
locale: Ref<string>;
|
|
23
|
+
}
|
|
24
|
+
/** Called once by the host. Pass `null` to detach (tests). */
|
|
25
|
+
export declare function setTranslator(translator: Translator | null): void;
|
|
26
|
+
/** Whether a host translator is installed. Rarely needed — prefer `translate`. */
|
|
27
|
+
export declare function hasTranslator(): boolean;
|
|
28
|
+
/** The host's current UI locale, or `'en'` when the host has no i18n. */
|
|
29
|
+
export declare function currentLocale(): string;
|
|
30
|
+
/**
|
|
31
|
+
* Translate through the host, or return the key unchanged.
|
|
32
|
+
*
|
|
33
|
+
* Reads `locale.value` before delegating so the caller picks up a reactive
|
|
34
|
+
* dependency on the locale even if the host's `t` were to memoize.
|
|
35
|
+
*/
|
|
36
|
+
export declare const translate: Translate;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { shallowRef } from "vue";
|
|
2
|
+
const registered = shallowRef(null);
|
|
3
|
+
export function setTranslator(translator) {
|
|
4
|
+
registered.value = translator;
|
|
5
|
+
}
|
|
6
|
+
export function hasTranslator() {
|
|
7
|
+
return registered.value !== null;
|
|
8
|
+
}
|
|
9
|
+
export function currentLocale() {
|
|
10
|
+
return registered.value?.locale.value ?? "en";
|
|
11
|
+
}
|
|
12
|
+
export const translate = ((key, ...args) => {
|
|
13
|
+
const translator = registered.value;
|
|
14
|
+
if (!translator) return key;
|
|
15
|
+
void translator.locale.value;
|
|
16
|
+
return translator.t(key, ...args);
|
|
17
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two international lists every studio ends up needing: countries
|
|
3
|
+
* (ISO 3166-1 alpha-2) and currencies (ISO 4217).
|
|
4
|
+
*
|
|
5
|
+
* They live in `studio-shared` rather than in a studio because a country is
|
|
6
|
+
* not a Commerce concern — Mail wants a sender country, Analytics wants a
|
|
7
|
+
* region breakdown, and whichever surface asked second would otherwise make
|
|
8
|
+
* the second copy. This module is that first copy, and the only one.
|
|
9
|
+
*
|
|
10
|
+
* ## Codes are data, names are derived
|
|
11
|
+
*
|
|
12
|
+
* The country CODES are written out here, because that literally is the
|
|
13
|
+
* standard: 249 two-letter identifiers, stable, and not enumerable from any
|
|
14
|
+
* runtime API. The NAMES are not written out — `Intl.DisplayNames` has them
|
|
15
|
+
* for every locale the browser ships, so a German cockpit reads
|
|
16
|
+
* "Vereinigtes Königreich" without anyone translating a hardcoded English
|
|
17
|
+
* list. Currencies need neither: `Intl.supportedValuesOf('currency')` is the
|
|
18
|
+
* code list, and `Intl.DisplayNames` is the names.
|
|
19
|
+
*
|
|
20
|
+
* The practical consequence is that adding a language costs nothing here.
|
|
21
|
+
*
|
|
22
|
+
* ## Sorted by the label, never by the code
|
|
23
|
+
*
|
|
24
|
+
* An operator scans a country list alphabetically by what they read.
|
|
25
|
+
* "Ägypten" belongs at the top of a German list and "Egypt" belongs under E;
|
|
26
|
+
* sorting by `EG` would put it in neither place. So every list is collated in
|
|
27
|
+
* the locale it is being displayed in, and the sort is redone per locale.
|
|
28
|
+
* The result is cached per locale — the collation is the expensive part and
|
|
29
|
+
* the answer never changes within a session.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The locale the lists are labelled in when a caller does not say. The cockpit
|
|
33
|
+
* is German-facing; when a real `useLocale()` lands, pass it through and this
|
|
34
|
+
* default stops being used.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DEFAULT_ISO_LOCALE = "de";
|
|
37
|
+
/** One choice: the stored code plus what a human reads in their language. */
|
|
38
|
+
export interface IsoOption {
|
|
39
|
+
/** The value written to the column — `'DE'`, `'EUR'`. */
|
|
40
|
+
code: string;
|
|
41
|
+
/** Localized name — `'Deutschland'`, `'Euro'`. Falls back to the code. */
|
|
42
|
+
label: string;
|
|
43
|
+
}
|
|
44
|
+
/** ISO 3166-1 alpha-2, unsorted. Use `countryOptions()` to display them. */
|
|
45
|
+
export declare function countryCodes(): readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* ISO 4217, as the runtime knows it. Empty on an engine without
|
|
48
|
+
* `Intl.supportedValuesOf` — callers treat that like any other unresolvable
|
|
49
|
+
* list and fall back to whatever literal options they were given.
|
|
50
|
+
*/
|
|
51
|
+
export declare function currencyCodes(): readonly string[];
|
|
52
|
+
/** Drop the memoised lists — for tests, and after a language switch. */
|
|
53
|
+
export declare function clearIsoListCache(): void;
|
|
54
|
+
/** Every country, sorted by its name in `locale`. */
|
|
55
|
+
export declare function countryOptions(locale?: string): IsoOption[];
|
|
56
|
+
/** Every currency the runtime knows, sorted by its name in `locale`. */
|
|
57
|
+
export declare function currencyOptions(locale?: string): IsoOption[];
|
|
58
|
+
/** `'DE'` → `'Deutschland'`. Unknown codes come back unchanged. */
|
|
59
|
+
export declare function countryLabel(code: unknown, locale?: string): string;
|
|
60
|
+
/** `'EUR'` → `'Euro'`. Unknown codes come back unchanged. */
|
|
61
|
+
export declare function currencyLabel(code: unknown, locale?: string): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const COUNTRY_CODES = Object.freeze(
|
|
2
|
+
"AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW".split(" ")
|
|
3
|
+
);
|
|
4
|
+
export const DEFAULT_ISO_LOCALE = "de";
|
|
5
|
+
export function countryCodes() {
|
|
6
|
+
return COUNTRY_CODES;
|
|
7
|
+
}
|
|
8
|
+
export function currencyCodes() {
|
|
9
|
+
const supported = Intl.supportedValuesOf;
|
|
10
|
+
if (typeof supported !== "function") return [];
|
|
11
|
+
try {
|
|
12
|
+
return Object.freeze(supported.call(Intl, "currency"));
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function displayNames(type, locale) {
|
|
18
|
+
try {
|
|
19
|
+
return new Intl.DisplayNames([locale], { type, fallback: "code" });
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function collator(locale) {
|
|
25
|
+
try {
|
|
26
|
+
return new Intl.Collator(locale);
|
|
27
|
+
} catch {
|
|
28
|
+
return { compare: (a, b) => a < b ? -1 : a > b ? 1 : 0 };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function build(codes, type, locale) {
|
|
32
|
+
const names = displayNames(type, locale);
|
|
33
|
+
const compare = collator(locale).compare;
|
|
34
|
+
return codes.map((code) => ({ code, label: names?.of(code) || code })).sort((a, b) => compare(a.label, b.label));
|
|
35
|
+
}
|
|
36
|
+
const cache = /* @__PURE__ */ new Map();
|
|
37
|
+
function cached(key, make) {
|
|
38
|
+
const hit = cache.get(key);
|
|
39
|
+
if (hit) return hit;
|
|
40
|
+
const built = make();
|
|
41
|
+
cache.set(key, built);
|
|
42
|
+
return built;
|
|
43
|
+
}
|
|
44
|
+
export function clearIsoListCache() {
|
|
45
|
+
cache.clear();
|
|
46
|
+
}
|
|
47
|
+
export function countryOptions(locale = DEFAULT_ISO_LOCALE) {
|
|
48
|
+
return cached(`region:${locale}`, () => build(COUNTRY_CODES, "region", locale));
|
|
49
|
+
}
|
|
50
|
+
export function currencyOptions(locale = DEFAULT_ISO_LOCALE) {
|
|
51
|
+
return cached(`currency:${locale}`, () => build(currencyCodes(), "currency", locale));
|
|
52
|
+
}
|
|
53
|
+
export function countryLabel(code, locale = DEFAULT_ISO_LOCALE) {
|
|
54
|
+
const key = typeof code === "string" ? code.trim().toUpperCase() : "";
|
|
55
|
+
if (!key) return "";
|
|
56
|
+
return displayNames("region", locale)?.of(key) || key;
|
|
57
|
+
}
|
|
58
|
+
export function currencyLabel(code, locale = DEFAULT_ISO_LOCALE) {
|
|
59
|
+
const key = typeof code === "string" ? code.trim().toUpperCase() : "";
|
|
60
|
+
if (!key) return "";
|
|
61
|
+
return displayNames("currency", locale)?.of(key) || key;
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SidebarNavItem } from '../types/nav.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure helpers behind `SidebarNav` / `SidebarNavLink`. They live outside the
|
|
4
|
+
* SFCs so the active-route and link-target rules can be tested without
|
|
5
|
+
* mounting a component (and so both SFCs share exactly one implementation).
|
|
6
|
+
*
|
|
7
|
+
* **`children` means tabs, not sub-navigation.** An item with children is one
|
|
8
|
+
* sidebar row; its children are the tabs of the page that row opens. Nothing
|
|
9
|
+
* here expands, so there is no collapse state to keep — the row is a link like
|
|
10
|
+
* any other, it just knows about the routes its tabs carry. Anything that
|
|
11
|
+
* should stand on its own in the sidebar is declared as its own item.
|
|
12
|
+
*/
|
|
13
|
+
/** Stable identity for `v-for` keys — `to` is unique where it exists. */
|
|
14
|
+
export declare function sidebarItemKey(item: SidebarNavItem): string;
|
|
15
|
+
/** True when `path` is the item's own route, or sits below it. */
|
|
16
|
+
export declare function sidebarItemIsActive(item: SidebarNavItem, path: string): boolean;
|
|
17
|
+
/** True when the item, or any of its tabs, matches `path`. */
|
|
18
|
+
export declare function sidebarItemHoldsActive(item: SidebarNavItem, path: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Which row gets the highlight. The tabs of a group are not rows of their own,
|
|
21
|
+
* so the group stays lit for every one of them — an operator on *Carriers*
|
|
22
|
+
* still sees where they are, which is **Shipping**.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sidebarItemIsCurrent(item: SidebarNavItem, path: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Where the row links. A group with a route of its own opens that (it is the
|
|
27
|
+
* default tab); a pure container — a heading several apps contribute to, where
|
|
28
|
+
* no single app's view is the right landing page — opens its first tab.
|
|
29
|
+
*
|
|
30
|
+
* `undefined` means the row has nothing to open at all. `navCompose` prunes
|
|
31
|
+
* those before they reach a sidebar, but the guard keeps a hand-written
|
|
32
|
+
* section (Settings, Apps, …) from rendering a dead row.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sidebarItemTarget(item: SidebarNavItem): string | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function sidebarItemKey(item) {
|
|
2
|
+
return item.key ?? item.to ?? item.label;
|
|
3
|
+
}
|
|
4
|
+
export function sidebarItemIsActive(item, path) {
|
|
5
|
+
if (!item.to) return false;
|
|
6
|
+
if (item.exact) return path === item.to;
|
|
7
|
+
return path === item.to || path.startsWith(`${item.to}/`);
|
|
8
|
+
}
|
|
9
|
+
export function sidebarItemHoldsActive(item, path) {
|
|
10
|
+
return sidebarItemIsActive(item, path) || (item.children ?? []).some((child) => sidebarItemHoldsActive(child, path));
|
|
11
|
+
}
|
|
12
|
+
export function sidebarItemIsCurrent(item, path) {
|
|
13
|
+
return sidebarItemHoldsActive(item, path);
|
|
14
|
+
}
|
|
15
|
+
export function sidebarItemTarget(item) {
|
|
16
|
+
if (item.to) return item.to;
|
|
17
|
+
for (const child of item.children ?? []) {
|
|
18
|
+
const target = sidebarItemTarget(child);
|
|
19
|
+
if (target) return target;
|
|
20
|
+
}
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ShallowRef } from 'vue';
|
|
2
|
+
import type { Talkback, TalkbackOptions } from '@revenexx/talkback-js';
|
|
3
|
+
export interface PlatformTalkbackConfig {
|
|
4
|
+
/** Centrifugo's client transport, e.g. `https://talkback.revenexx.com`. */
|
|
5
|
+
host: string;
|
|
6
|
+
/**
|
|
7
|
+
* Where a connection token is minted. The facade's own `${host}/v1/tokens` by
|
|
8
|
+
* default — the browser's Zitadel token authorises it directly, so no BFF route
|
|
9
|
+
* stands in between.
|
|
10
|
+
*/
|
|
11
|
+
tokenEndpoint: string;
|
|
12
|
+
/** Likewise, `${host}/v1/subscription-tokens`. */
|
|
13
|
+
subscriptionTokenEndpoint: string;
|
|
14
|
+
/**
|
|
15
|
+
* The active tenant slug. A GETTER, not a value: operator impersonation
|
|
16
|
+
* changes it at run time, and it is positionally first in every channel name.
|
|
17
|
+
*/
|
|
18
|
+
tenant: () => string;
|
|
19
|
+
/**
|
|
20
|
+
* The caller's Zitadel access token, passed to the facade as the bearer.
|
|
21
|
+
*
|
|
22
|
+
* The facade admits it because an end user holds `user` or `admin` in the
|
|
23
|
+
* Zitadel USER project, and such a caller may mint only for itself: the request
|
|
24
|
+
* carries no `user_id` and the facade fills it from this token's own `sub`.
|
|
25
|
+
*/
|
|
26
|
+
getToken: () => Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Replaces the Centrifuge client. Left unset outside tests — the fake in
|
|
29
|
+
* `@revenexx/talkback-js/testing` satisfies it.
|
|
30
|
+
*/
|
|
31
|
+
client?: TalkbackOptions['client'];
|
|
32
|
+
debug?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The `sub` claim of a JWT, or null when it isn't one.
|
|
36
|
+
*
|
|
37
|
+
* This is where the client's `userId` comes from, and the choice is
|
|
38
|
+
* load-bearing rather than convenient. The BFF derives the token's subject from
|
|
39
|
+
* the same claim of the same token, so client and server cannot disagree about
|
|
40
|
+
* the name of the `user:` channel — a mismatch would mint a grant for one name
|
|
41
|
+
* and subscribe to another, which fails as a silent 403 on a channel nobody
|
|
42
|
+
* publishes to. Deliberately NOT verified: nothing is authorised here, and the
|
|
43
|
+
* BFF verifies the signature before it grants anything.
|
|
44
|
+
*
|
|
45
|
+
* Mirrors the base64url handling in the cockpit's `useConsoleAuth`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function decodeJwtSub(jwt: string | null | undefined): string | null;
|
|
48
|
+
/**
|
|
49
|
+
* Builds the client for a host.
|
|
50
|
+
*
|
|
51
|
+
* `tenant` and `userId` are providers because talkback-js reads them on every
|
|
52
|
+
* channel build; passing values would pin the first tenant the page saw.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createPlatformTalkback(config: PlatformTalkbackConfig): Promise<Talkback>;
|
|
55
|
+
/** Registered once by the host's provider. Pass `null` to detach (tests). */
|
|
56
|
+
export declare function setTalkback(tb: Talkback | null): void;
|
|
57
|
+
export declare function getTalkback(): Talkback | null;
|
|
58
|
+
/** The registry itself, for composables that must react to it appearing. */
|
|
59
|
+
export declare function talkbackRef(): ShallowRef<Talkback | null>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { shallowRef } from "vue";
|
|
2
|
+
export function decodeJwtSub(jwt) {
|
|
3
|
+
if (!jwt) return null;
|
|
4
|
+
try {
|
|
5
|
+
const parts = jwt.split(".");
|
|
6
|
+
if (parts.length !== 3) return null;
|
|
7
|
+
const p = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
8
|
+
const b64 = p.padEnd(p.length + (4 - p.length % 4) % 4, "=");
|
|
9
|
+
const claims = JSON.parse(atob(b64));
|
|
10
|
+
return typeof claims.sub === "string" && claims.sub ? claims.sub : null;
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function createPlatformTalkback(config) {
|
|
16
|
+
const { createTalkback } = await import("@revenexx/talkback-js");
|
|
17
|
+
let userId = "";
|
|
18
|
+
const accessToken = async () => {
|
|
19
|
+
const token = await config.getToken();
|
|
20
|
+
if (token) {
|
|
21
|
+
userId = decodeJwtSub(token) ?? userId;
|
|
22
|
+
}
|
|
23
|
+
return token;
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
userId = decodeJwtSub(await config.getToken()) ?? "";
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
return createTalkback({
|
|
30
|
+
host: config.host,
|
|
31
|
+
tenant: () => config.tenant(),
|
|
32
|
+
userId: () => userId,
|
|
33
|
+
tokenEndpoint: config.tokenEndpoint,
|
|
34
|
+
subscriptionTokenEndpoint: config.subscriptionTokenEndpoint,
|
|
35
|
+
accessToken,
|
|
36
|
+
...config.client ? { client: config.client } : {},
|
|
37
|
+
...config.debug === void 0 ? {} : { debug: config.debug }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const current = shallowRef(null);
|
|
41
|
+
export function setTalkback(tb) {
|
|
42
|
+
current.value = tb;
|
|
43
|
+
}
|
|
44
|
+
export function getTalkback() {
|
|
45
|
+
return current.value;
|
|
46
|
+
}
|
|
47
|
+
export function talkbackRef() {
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
@@ -7,13 +7,27 @@ import type { Component } from 'vue';
|
|
|
7
7
|
*/
|
|
8
8
|
export type SidebarNavItem = {
|
|
9
9
|
label: string;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Target route. Omit it for a pure container — a heading several apps
|
|
12
|
+
* contribute pages to, where no single view is the right landing page. The
|
|
13
|
+
* row then links to the first of its `children` instead.
|
|
14
|
+
*/
|
|
15
|
+
to?: string;
|
|
11
16
|
icon?: Component;
|
|
12
17
|
badge?: number | string | null;
|
|
13
18
|
/** Dim + show a "soon" tag (not-yet-shipped surfaces). */
|
|
14
19
|
preview?: boolean;
|
|
15
20
|
/** Match the route exactly (for studio roots like `/settings`). */
|
|
16
21
|
exact?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The item's **tabs**, not sub-navigation. The sidebar shows one row; the
|
|
24
|
+
* page it opens shows these across the top, each addressed by its own route
|
|
25
|
+
* (see `studio-commerce/lib/navGroups.ts`). The row stays highlighted on all
|
|
26
|
+
* of them.
|
|
27
|
+
*/
|
|
28
|
+
children?: SidebarNavItem[];
|
|
29
|
+
/** Stable `v-for` key. Defaults to `to ?? label`. */
|
|
30
|
+
key?: string;
|
|
17
31
|
};
|
|
18
32
|
export type SidebarNavSection = {
|
|
19
33
|
key: string;
|