@revenexx/studio-shared 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/module.d.mts +1 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +5 -0
  4. package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -0
  5. package/dist/runtime/components/DataTable/ColumnsMenu.vue +90 -34
  6. package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -0
  7. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +23 -2
  8. package/dist/runtime/components/DataTable/DataTable.vue +199 -100
  9. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +23 -2
  10. package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +5 -3
  11. package/dist/runtime/components/DataTable/FilterMenu.vue +32 -36
  12. package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +5 -3
  13. package/dist/runtime/components/DataTable/types.d.ts +15 -2
  14. package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +55 -0
  15. package/dist/runtime/components/Shared/FloatingPanel.vue +118 -0
  16. package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +55 -0
  17. package/dist/runtime/composables/useFloatingPanel.d.ts +69 -0
  18. package/dist/runtime/composables/useFloatingPanel.js +112 -0
  19. package/dist/runtime/composables/useTalkbackChannels.d.ts +21 -6
  20. package/dist/runtime/composables/useTalkbackChannels.js +16 -12
  21. package/dist/runtime/lib/columnOrder.d.ts +45 -0
  22. package/dist/runtime/lib/columnOrder.js +28 -0
  23. package/dist/runtime/lib/columnVisibility.d.ts +41 -0
  24. package/dist/runtime/lib/columnVisibility.js +12 -0
  25. package/dist/runtime/lib/dataTableFilters.d.ts +83 -0
  26. package/dist/runtime/lib/dataTableFilters.js +23 -0
  27. package/dist/runtime/lib/i18nBridge.d.ts +7 -2
  28. package/dist/types.d.mts +2 -0
  29. package/package.json +2 -2
@@ -1,13 +1,13 @@
1
- import { computed, effectScope, onScopeDispose, ref, shallowRef, watch } from "vue";
1
+ import { computed, effectScope, onScopeDispose, shallowRef, watch, watchEffect } from "vue";
2
2
  import { talkbackRef } from "../lib/talkback.js";
3
3
  function useDelegate(subscribe) {
4
4
  const client = talkbackRef();
5
- const name = ref(null);
5
+ const attached = shallowRef(null);
6
6
  const scope = shallowRef(null);
7
7
  function stop() {
8
8
  scope.value?.stop();
9
9
  scope.value = null;
10
- name.value = null;
10
+ attached.value = null;
11
11
  }
12
12
  const unwatch = watch(client, (tb) => {
13
13
  stop();
@@ -18,14 +18,18 @@ function useDelegate(subscribe) {
18
18
  const vue = await import("@revenexx/talkback-js/vue");
19
19
  if (scope.value !== child) return;
20
20
  child.run(() => {
21
- let sub;
22
- try {
23
- sub = subscribe(vue, tb);
24
- } catch {
25
- name.value = null;
26
- return;
27
- }
28
- watch(() => sub.channel.value, (v) => name.value = v, { immediate: true, flush: "sync" });
21
+ let sub = null;
22
+ watchEffect(() => {
23
+ if (sub) return;
24
+ child.run(() => {
25
+ try {
26
+ sub = subscribe(vue, tb);
27
+ } catch {
28
+ return;
29
+ }
30
+ attached.value = sub;
31
+ });
32
+ });
29
33
  });
30
34
  })();
31
35
  }, { immediate: true });
@@ -33,7 +37,7 @@ function useDelegate(subscribe) {
33
37
  unwatch();
34
38
  stop();
35
39
  });
36
- return { channel: computed(() => name.value), stop };
40
+ return { channel: computed(() => attached.value?.channel.value ?? null), stop };
37
41
  }
38
42
  export function useTalkbackTopic(topic, options = {}) {
39
43
  return useDelegate((vue, tb) => vue.useTalkbackTopic(topic, { ...options, talkback: tb }));
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The order a table's columns are drawn in, once the reader has an opinion.
3
+ *
4
+ * Sits beside `columnVisibility` because it has the SAME trap, and the trap has
5
+ * already shipped once in this file's neighbour.
6
+ *
7
+ * A table's column list is not fixed for the life of the component. A tab that
8
+ * offers Markets only once the service proves it has the column, or engagement
9
+ * once the deployment reports any, hands the table a LONGER array on a later
10
+ * render. If the remembered order were treated as the whole truth, every column
11
+ * that turned up afterwards would be missing from it — and the obvious
12
+ * implementation, "sort by index in the remembered list, unknowns last", puts
13
+ * that new column silently at the far right where nobody scrolls. Which is
14
+ * indistinguishable, from the outside, from never having been built.
15
+ *
16
+ * So what is remembered is the reader's own arrangement, and a column that is
17
+ * not in it keeps its DECLARED neighbours: it lands directly after whichever
18
+ * declared predecessor is nearest, or first if it has none. A column added
19
+ * between Channel and Status appears between Channel and Status, wherever the
20
+ * reader has since dragged those two.
21
+ */
22
+ export interface OrderableColumn {
23
+ key: string;
24
+ }
25
+ /**
26
+ * The declared columns, arranged.
27
+ *
28
+ * An empty `order` means the reader has not touched anything, which is not the
29
+ * same as an order that happens to match — the first is "use the declaration",
30
+ * the second is a decision that must survive the declaration changing.
31
+ */
32
+ export declare function orderColumnsBy<T extends OrderableColumn>(columns: readonly T[], order: readonly string[]): T[];
33
+ /**
34
+ * Move one column and return the arrangement to remember.
35
+ *
36
+ * Always a COMPLETE list of keys, even when the previous one was partial or
37
+ * empty: after the first drag there is an opinion about every column, and a
38
+ * half-remembered order would leave the untouched ones re-deriving their
39
+ * position on every render.
40
+ *
41
+ * `to` is the index in the list as it will be — what a drop target means — and
42
+ * out-of-range values clamp rather than throw. A drag that ends outside the
43
+ * list means "leave it where it is", not "crash the picker".
44
+ */
45
+ export declare function reorderColumns<T extends OrderableColumn>(columns: readonly T[], order: readonly string[], from: number, to: number): string[];
@@ -0,0 +1,28 @@
1
+ export function orderColumnsBy(columns, order) {
2
+ if (!order.length) return [...columns];
3
+ const byKey = new Map(columns.map((c) => [c.key, c]));
4
+ const remembered = new Set(order);
5
+ const result = order.filter((k) => byKey.has(k)).map((k) => byKey.get(k));
6
+ columns.forEach((column, declaredIndex) => {
7
+ if (remembered.has(column.key)) return;
8
+ let insertAt = 0;
9
+ for (let i = declaredIndex - 1; i >= 0; i--) {
10
+ const at = result.indexOf(columns[i]);
11
+ if (at !== -1) {
12
+ insertAt = at + 1;
13
+ break;
14
+ }
15
+ }
16
+ result.splice(insertAt, 0, column);
17
+ });
18
+ return result;
19
+ }
20
+ export function reorderColumns(columns, order, from, to) {
21
+ const keys = orderColumnsBy(columns, order).map((c) => c.key);
22
+ if (from < 0 || from >= keys.length) return keys;
23
+ const clamped = Math.max(0, Math.min(to, keys.length - 1));
24
+ if (clamped === from) return keys;
25
+ const [moved] = keys.splice(from, 1);
26
+ keys.splice(clamped, 0, moved);
27
+ return keys;
28
+ }
@@ -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
+ }
@@ -3,13 +3,18 @@ import type { Ref } from 'vue';
3
3
  export type TranslateNamed = Record<string, unknown>;
4
4
  /**
5
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, plural, named)`.
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.
7
12
  */
8
13
  export interface Translate {
9
14
  (key: string): string;
10
15
  (key: string, named: TranslateNamed): string;
11
16
  (key: string, plural: number): string;
12
- (key: string, plural: number, named: TranslateNamed): string;
17
+ (key: string, named: TranslateNamed, plural: number): string;
13
18
  }
14
19
  export interface Translator {
15
20
  t: Translate;
package/dist/types.d.mts CHANGED
@@ -16,6 +16,8 @@ export { type TalkbackChannel, type TalkbackChannelOptions, type TalkbackMaybeRe
16
16
 
17
17
  export { type MediaLibraryClient } from '../dist/runtime/composables/useMediaLibrary.js'
18
18
 
19
+ export { type FloatingPanelDock, type FloatingPanelMode, type FloatingPanelOptions } from '../dist/runtime/composables/useFloatingPanel.js'
20
+
19
21
  export { type IsoOption } from '../dist/runtime/lib/isoLists.js'
20
22
 
21
23
  export { type Translate, type TranslateNamed, type Translator } from '../dist/runtime/lib/i18nBridge.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/studio-shared",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Shared revenexx Studio shell primitives (PageHeader, SidebarNav, StateCard, DataTable, …) + the platform provider contract (usePlatformAuth/usePlatformTenant). A Nuxt module — add it to `modules`.",
6
6
  "main": "./dist/module.mjs",
@@ -28,7 +28,7 @@
28
28
  "@nuxt/kit": "^4.0.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@revenexx/studio": "*",
31
+ "@revenexx/studio": "^0.2.0",
32
32
  "@revenexx/talkback-js": "^0.3.0",
33
33
  "@solar-icons/nuxt": "^1.2.0",
34
34
  "centrifuge": ">=5.2.0",