@revenexx/studio-shared 0.4.0 → 0.4.2

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 (110) hide show
  1. package/dist/module.d.mts +5 -3
  2. package/dist/module.json +2 -2
  3. package/dist/module.mjs +7 -3
  4. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +1 -1
  5. package/dist/runtime/components/Dashboard/DashboardGrid.vue +45 -1
  6. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +1 -1
  7. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +1 -1
  8. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +1 -1
  9. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +1 -1
  10. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +2 -2
  11. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +14 -3
  12. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +2 -2
  13. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +73 -34
  14. package/dist/runtime/components/DataTable/DataTable.vue +352 -52
  15. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +73 -34
  16. package/dist/runtime/components/DataTable/FacetPanel.vue +57 -8
  17. package/dist/runtime/components/DataTable/FilterMenu.vue +5 -5
  18. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +61 -12
  19. package/dist/runtime/components/DataTable/SortMenu.vue +4 -1
  20. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.d.vue.ts +23 -0
  21. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue +29 -0
  22. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue.d.ts +23 -0
  23. package/dist/runtime/components/DataTable/types.d.ts +17 -0
  24. package/dist/runtime/components/Form/FieldGrid.vue +1 -1
  25. package/dist/runtime/components/Form/SaveBar.d.vue.ts +1 -1
  26. package/dist/runtime/components/Form/SaveBar.vue.d.ts +1 -1
  27. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +4 -4
  28. package/dist/runtime/components/Media/MediaFolderTree.vue +1 -2
  29. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +4 -4
  30. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +11 -0
  31. package/dist/runtime/components/Media/MediaPickerDialog.vue +206 -54
  32. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +11 -0
  33. package/dist/runtime/components/Media/MediaUploader.vue +1 -1
  34. package/dist/runtime/components/Shared/AppIcon.d.vue.ts +1 -1
  35. package/dist/runtime/components/Shared/AppIcon.vue.d.ts +1 -1
  36. package/dist/runtime/components/Shared/CardSizeToggle.d.vue.ts +11 -0
  37. package/dist/runtime/components/Shared/CardSizeToggle.vue +19 -0
  38. package/dist/runtime/components/Shared/CardSizeToggle.vue.d.ts +11 -0
  39. package/dist/runtime/components/Shared/DetailSheet.d.vue.ts +1 -1
  40. package/dist/runtime/components/Shared/DetailSheet.vue +9 -5
  41. package/dist/runtime/components/Shared/DetailSheet.vue.d.ts +1 -1
  42. package/dist/runtime/components/Shared/EmptyNote.d.vue.ts +19 -0
  43. package/dist/runtime/components/Shared/EmptyNote.vue +25 -0
  44. package/dist/runtime/components/Shared/EmptyNote.vue.d.ts +19 -0
  45. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +6 -0
  46. package/dist/runtime/components/Shared/KpiTile.vue +10 -9
  47. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +6 -0
  48. package/dist/runtime/components/Shared/PageHeader.vue +15 -4
  49. package/dist/runtime/components/Shared/RecordLayout.vue +1 -2
  50. package/dist/runtime/components/Shared/RecordRailCard.vue +3 -3
  51. package/dist/runtime/components/Shared/SelectionBar.d.vue.ts +22 -0
  52. package/dist/runtime/components/Shared/SelectionBar.vue +40 -0
  53. package/dist/runtime/components/Shared/SelectionBar.vue.d.ts +22 -0
  54. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +36 -2
  55. package/dist/runtime/components/Shared/SidebarNav.vue +74 -25
  56. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +36 -2
  57. package/dist/runtime/components/Shared/SidebarNavAreas.d.vue.ts +7 -0
  58. package/dist/runtime/components/Shared/SidebarNavAreas.vue +291 -0
  59. package/dist/runtime/components/Shared/SidebarNavAreas.vue.d.ts +7 -0
  60. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +1 -9
  61. package/dist/runtime/components/Shared/SidebarNavLink.vue +31 -5
  62. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +1 -9
  63. package/dist/runtime/components/Shared/SortHeader.vue +6 -2
  64. package/dist/runtime/components/Shared/StateCard.vue +1 -1
  65. package/dist/runtime/components/Shared/TabBar.vue +6 -7
  66. package/dist/runtime/components/Shared/Wizard.d.vue.ts +11 -1
  67. package/dist/runtime/components/Shared/Wizard.vue +9 -5
  68. package/dist/runtime/components/Shared/Wizard.vue.d.ts +11 -1
  69. package/dist/runtime/composables/useMediaLibrary.d.ts +12 -0
  70. package/dist/runtime/composables/useSavedViews.d.ts +11 -1
  71. package/dist/runtime/composables/useSavedViews.js +4 -4
  72. package/dist/runtime/composables/useWidgetFigures.d.ts +12 -0
  73. package/dist/runtime/composables/useWidgetFigures.js +24 -0
  74. package/dist/runtime/composables/useWidgetHeader.d.ts +15 -0
  75. package/dist/runtime/composables/useWidgetHeader.js +15 -0
  76. package/dist/runtime/lib/auditEnvelope.d.ts +43 -0
  77. package/dist/runtime/lib/auditEnvelope.js +55 -0
  78. package/dist/runtime/lib/cardSize.d.ts +13 -0
  79. package/dist/runtime/lib/cardSize.js +20 -0
  80. package/dist/runtime/lib/dashboardIcons.js +2 -0
  81. package/dist/runtime/lib/dataTableFilters.d.ts +22 -0
  82. package/dist/runtime/lib/dataTableFilters.js +5 -0
  83. package/dist/runtime/lib/facets.d.ts +13 -0
  84. package/dist/runtime/lib/formLayout.d.ts +74 -4
  85. package/dist/runtime/lib/formLayout.js +53 -6
  86. package/dist/runtime/lib/gridTracks.d.ts +14 -1
  87. package/dist/runtime/lib/gridTracks.js +13 -2
  88. package/dist/runtime/lib/isoLists.d.ts +37 -2
  89. package/dist/runtime/lib/isoLists.js +54 -1
  90. package/dist/runtime/lib/notice.d.ts +14 -0
  91. package/dist/runtime/lib/notice.js +19 -0
  92. package/dist/runtime/lib/pageSize.d.ts +15 -0
  93. package/dist/runtime/lib/pageSize.js +26 -0
  94. package/dist/runtime/lib/sidebarAreas.d.ts +46 -0
  95. package/dist/runtime/lib/sidebarAreas.js +25 -0
  96. package/dist/runtime/lib/sidebarNav.d.ts +39 -2
  97. package/dist/runtime/lib/sidebarNav.js +30 -4
  98. package/dist/runtime/lib/solarIconMap.d.ts +2 -1
  99. package/dist/runtime/lib/solarIconMap.js +50 -4
  100. package/dist/runtime/lib/swipeToClose.d.ts +25 -0
  101. package/dist/runtime/lib/swipeToClose.js +118 -0
  102. package/dist/runtime/lib/tableExport.d.ts +31 -0
  103. package/dist/runtime/lib/tableExport.js +143 -0
  104. package/dist/runtime/lib/talkback.d.ts +5 -0
  105. package/dist/runtime/lib/talkback.js +8 -0
  106. package/dist/runtime/lib/talkbackTokenBatch.d.ts +35 -0
  107. package/dist/runtime/lib/talkbackTokenBatch.js +101 -0
  108. package/dist/runtime/types/nav.d.ts +9 -0
  109. package/dist/types.d.mts +4 -0
  110. package/package.json +4 -3
@@ -1,9 +1,10 @@
1
- import { Eye } from '#solar-icons';
1
+ import type { Facet, FacetSelection } from '../../lib/facets.js';
2
+ import type { DataTableColumn, DataTableFilter, DataTableRail, DataTableSavedViews, DataTableState, DataTableViewMode, RowKey, SortState } from './types.js';
2
3
  declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
3
4
  props: import("vue").PublicProps & __VLS_PrettifyLocal<({
4
5
  data: T[];
5
- columns: Eye<T>[];
6
- rowKey: Eye<T>;
6
+ columns: DataTableColumn<T>[];
7
+ rowKey: RowKey<T>;
7
8
  /** `label` is the field's accessible name; it defaults to the placeholder
8
9
  * where the surface wrote one (see `searchLabel`). */
9
10
  search?: boolean | {
@@ -20,7 +21,7 @@ declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_se
20
21
  };
21
22
  /** Built-in status/dimension filter pills (static mode). One dimension, or
22
23
  * several (each renders as its own sub-menu). */
23
- filterTabs?: Eye<T> | Eye<T>[];
24
+ filterTabs?: DataTableFilter<T> | DataTableFilter<T>[];
24
25
  /**
25
26
  * The surface's declared dimensions, as one facet model (PE-6xx).
26
27
  *
@@ -37,17 +38,17 @@ declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_se
37
38
  * second placement: a tree pinned in a left column beside a panel is two
38
39
  * places to look and two commit rules for one question.
39
40
  */
40
- facets?: Eye[];
41
+ facets?: Facet[];
41
42
  /** Say which facet each chip belongs to (see `FacetChips.showFacet`). */
42
43
  chipFacetLabels?: boolean;
43
44
  /** Saved views control — the surface supplies snapshot/apply, the table
44
45
  * owns the control, the storage and the dialogs. */
45
- savedViews?: Eye | null;
46
+ savedViews?: DataTableSavedViews | null;
46
47
  /** View-mode switch (e.g. List | Gallery). Any mode other than `list`
47
48
  * renders the `#view` slot in place of the table body. */
48
- viewModes?: Eye[] | null;
49
+ viewModes?: DataTableViewMode[] | null;
49
50
  /** Collapse handle for the surface's left rail (see `ListSurface`). */
50
- rail?: boolean | Eye;
51
+ rail?: boolean | DataTableRail;
51
52
  /**
52
53
  * Scroll the ROWS inside the table instead of the page.
53
54
  *
@@ -73,6 +74,12 @@ declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_se
73
74
  scrollBody?: boolean | "fit" | {
74
75
  maxHeight?: string;
75
76
  };
77
+ /**
78
+ * Extra room `'fit'` keeps free under the table — for a bar the host floats
79
+ * over the bottom of the list while rows are selected, so it never covers
80
+ * the pagination row (Re-QA 18.10.).
81
+ */
82
+ reserveBottom?: number;
76
83
  pagination?: boolean | {
77
84
  pageSize?: number;
78
85
  };
@@ -110,18 +117,51 @@ declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_se
110
117
  * above in the sequence.
111
118
  */
112
119
  reorderDisabled?: string | null;
120
+ /**
121
+ * Below 768px every row is drawn as a card — title, status badge, two lines
122
+ * of key facts — instead of a grid two columns of which fit (R4, Charge 2).
123
+ * Off for a table that is already narrow by nature.
124
+ */
125
+ mobileCards?: boolean;
126
+ /** `localStorage` key the row-height choice is remembered under (D4). */
127
+ densityKey?: string;
128
+ /**
129
+ * Which table the rows-per-page choice is remembered for (PE-1052). Falls
130
+ * back to the saved views' `storageKey`; with neither, the size is not kept.
131
+ */
132
+ pageSizeKey?: string | null;
133
+ /**
134
+ * A table set INTO a card on a detail page (Michael 14.10.): no frame or
135
+ * corners of its own, the grey head row running edge to edge, and the
136
+ * search row plain white — the tinted toolbar is a list page's chrome, and
137
+ * inside a card it read as a second box with too little contrast.
138
+ */
139
+ embedded?: boolean;
140
+ /**
141
+ * Draw the control row at all. A detail-page card hides it until the
142
+ * operator asks (the filter toggle in the card head, Michael 14.10.), so a
143
+ * table of three rows is three rows and not a toolbar with a table under it.
144
+ */
145
+ toolbar?: boolean;
113
146
  } & {
114
- sort?: Eye;
147
+ query?: string;
148
+ sort?: SortState;
115
149
  page?: number;
116
150
  viewMode?: string;
117
151
  railOpen?: boolean;
152
+ /**
153
+ * Row height (D4, Charge 2). Standard is 44px with a second line only in the
154
+ * identifying column; compact is 36px and one line everywhere. The reader's
155
+ * choice, remembered across lists — it is about their screen, not one list.
156
+ */
157
+ density?: "standard" | "compact";
118
158
  /**
119
159
  * The committed facet selection. Two-way so a chip removed under the toolbar,
120
160
  * a facet picked in the surface's pinned rail and the panel's own Apply all
121
161
  * write to one place — and so the surface can seed it from a saved view or a
122
162
  * URL without this component holding a second copy to drift from.
123
163
  */
124
- facetSelection?: Eye;
164
+ facetSelection?: FacetSelection;
125
165
  }) & {
126
166
  onRowClick?: ((row: T) => any) | undefined;
127
167
  onStateChange?: ((state: DataTableState) => any) | undefined;
@@ -132,50 +172,49 @@ declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_se
132
172
  to: number;
133
173
  keys: (string | number)[];
134
174
  }) => any) | undefined;
175
+ "onUpdate:query"?: ((value: string) => any) | undefined;
135
176
  "onUpdate:sort"?: ((value: SortState) => any) | undefined;
136
177
  "onUpdate:page"?: ((value: number) => any) | undefined;
137
178
  "onUpdate:viewMode"?: ((value: string) => any) | undefined;
138
179
  "onUpdate:railOpen"?: ((value: boolean) => any) | undefined;
180
+ "onUpdate:density"?: ((value: "compact" | "standard") => any) | undefined;
139
181
  "onUpdate:facetSelection"?: ((value: FacetSelection) => any) | undefined;
140
182
  }> & (typeof globalThis extends {
141
183
  __VLS_PROPS_FALLBACK: infer P;
142
184
  } ? P : {});
143
- expose: (exposed: {}) => void;
185
+ expose: (exposed: import("vue").ShallowUnwrapRef<{
186
+ visibleColumns: any;
187
+ }>) => void;
144
188
  attrs: any;
145
189
  slots: {
146
- [x: `header-${any}`]: ((props: {}) => any) | undefined;
147
- } & {
148
- [x: `cell-${any}`]: ((props: {
149
- row: any;
150
- }) => any) | undefined;
151
- } & {
152
- 'filter-menu'?: (props: {}) => any;
153
- } & {
154
- scope?: (props: {}) => any;
155
- } & {
156
- 'toolbar-left'?: (props: {}) => any;
157
- } & {
158
- 'toolbar-right'?: (props: {}) => any;
159
- } & {
160
- actions?: (props: {}) => any;
161
- } & {
190
+ [name: `header-${string}`]: (props: Record<string, never>) => unknown;
191
+ [name: `cell-${string}`]: (props: {
192
+ row: T;
193
+ }) => unknown;
194
+ 'filter-menu'?: (props: Record<string, never>) => unknown;
195
+ scope?: (props: Record<string, never>) => unknown;
196
+ 'toolbar-left'?: (props: Record<string, never>) => unknown;
197
+ 'toolbar-right'?: (props: Record<string, never>) => unknown;
198
+ actions?: (props: Record<string, never>) => unknown;
162
199
  view?: (props: {
163
- rows: any;
200
+ rows: T[];
164
201
  loading: boolean;
165
- }) => any;
166
- } & {
202
+ }) => unknown;
167
203
  empty?: (props: {
168
- query: any;
169
- filters: any;
204
+ query: string;
205
+ filters: {
206
+ label: string;
207
+ values: string[];
208
+ }[];
170
209
  clear: () => void;
171
- }) => any;
210
+ }) => unknown;
172
211
  };
173
212
  emit: (((evt: "rowClick", row: T) => void) & ((evt: "stateChange", state: DataTableState) => void) & ((evt: "orderChange", keys: (string | number)[]) => void) & ((evt: "rowMove", move: {
174
213
  key: string | number;
175
214
  from: number;
176
215
  to: number;
177
216
  keys: (string | number)[];
178
- }) => void)) & (((event: "update:sort", value: SortState) => void) & ((event: "update:page", value: number) => void) & ((event: "update:viewMode", value: string) => void) & ((event: "update:railOpen", value: boolean) => void) & ((event: "update:facetSelection", value: FacetSelection) => void));
217
+ }) => void)) & (((event: "update:query", value: string) => void) & ((event: "update:sort", value: SortState) => void) & ((event: "update:page", value: number) => void) & ((event: "update:viewMode", value: string) => void) & ((event: "update:railOpen", value: boolean) => void) & ((event: "update:density", value: "compact" | "standard") => void) & ((event: "update:facetSelection", value: FacetSelection) => void));
179
218
  }>) => import("vue").VNode & {
180
219
  __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
181
220
  };
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, ref, watch } from "vue";
2
+ import { computed, onBeforeUnmount, ref, watch } from "vue";
3
3
  import { AltArrowDown, AltArrowRight, Filter, Magnifier } from "#solar-icons";
4
4
  import {
5
5
  Button,
@@ -7,9 +7,9 @@ import {
7
7
  Input,
8
8
  Popover,
9
9
  PopoverContent,
10
- PopoverTrigger,
11
- ToolbarFilterTrigger
10
+ PopoverTrigger
12
11
  } from "@revenexx/studio";
12
+ import ToolbarLabelTrigger from "./ToolbarLabelTrigger.vue";
13
13
  import {
14
14
  allNodes,
15
15
  copySelection,
@@ -57,12 +57,54 @@ function matches(facet, label) {
57
57
  return label.toLowerCase().includes(query.value);
58
58
  }
59
59
  function visibleOptions(facet) {
60
+ if (facet.search) return searchedOptions(facet);
60
61
  return (facet.options ?? []).filter((option) => matches(facet, option.label));
61
62
  }
63
+ const remote = ref({});
64
+ const pickedHits = ref({});
65
+ let remoteTimer = null;
66
+ let remoteSeq = 0;
67
+ onBeforeUnmount(() => {
68
+ if (remoteTimer) clearTimeout(remoteTimer);
69
+ });
70
+ function remoteTerm(facet) {
71
+ return !query.value || facet.label.toLowerCase().includes(query.value) ? "" : search.value.trim();
72
+ }
73
+ async function runRemote(facet) {
74
+ if (!facet.search) return;
75
+ const mine = ++remoteSeq;
76
+ remote.value = { ...remote.value, [facet.key]: { options: remote.value[facet.key]?.options ?? [], busy: true } };
77
+ let options = [];
78
+ try {
79
+ options = await facet.search(remoteTerm(facet));
80
+ } catch {
81
+ options = [];
82
+ }
83
+ if (mine !== remoteSeq) return;
84
+ remote.value = { ...remote.value, [facet.key]: { options, busy: false } };
85
+ }
86
+ watch([() => activeFacet.value?.key, query, open], () => {
87
+ const facet = activeFacet.value;
88
+ if (!open.value || !facet?.search) return;
89
+ if (remoteTimer) clearTimeout(remoteTimer);
90
+ remoteTimer = setTimeout(() => {
91
+ void runRemote(facet);
92
+ }, 250);
93
+ }, { immediate: true });
94
+ function searchedOptions(facet) {
95
+ const ticked = draft.value[facet.key];
96
+ const values = Array.isArray(ticked) ? ticked : [];
97
+ const named = new Map((facet.options ?? []).map((o) => [o.value, o]));
98
+ for (const [value, option] of Object.entries(pickedHits.value[facet.key] ?? {})) named.set(value, option);
99
+ const head = values.map((value) => named.get(value) ?? { value, label: value });
100
+ const seen = new Set(values);
101
+ return [...head, ...(remote.value[facet.key]?.options ?? []).filter((o) => !seen.has(o.value))];
102
+ }
62
103
  const visibleFacets = computed(() => {
63
104
  if (!query.value) return panelFacets.value;
64
105
  return panelFacets.value.filter((facet) => {
65
106
  if (facet.label.toLowerCase().includes(query.value)) return true;
107
+ if (facet.search) return true;
66
108
  if (facet.kind === "check") return visibleOptions(facet).length > 0;
67
109
  if (facet.kind === "tree") return visibleNodes(facet).length > 0;
68
110
  return false;
@@ -73,6 +115,8 @@ function isTicked(key, value) {
73
115
  return Array.isArray(current) && current.includes(value);
74
116
  }
75
117
  function tick(key, value, on) {
118
+ const hit = remote.value[key]?.options.find((o) => o.value === value);
119
+ if (on && hit) pickedHits.value = { ...pickedHits.value, [key]: { ...pickedHits.value[key] ?? {}, [value]: hit } };
76
120
  const single = props.facets.find((f) => f.key === key)?.single;
77
121
  draft.value = toggleFacetValue(draft.value, key, value, on, { single });
78
122
  }
@@ -138,8 +182,7 @@ const nothingToReset = computed(() => Object.keys(draft.value).every((key) => is
138
182
  <template>
139
183
  <Popover v-model:open="open">
140
184
  <PopoverTrigger as-child>
141
- <ToolbarFilterTrigger
142
- icon-only
185
+ <ToolbarLabelTrigger
143
186
  :active="appliedCount > 0"
144
187
  :count="appliedCount"
145
188
  :label="t('shared.facetpanel.trigger.label')"
@@ -148,7 +191,7 @@ const nothingToReset = computed(() => Object.keys(draft.value).every((key) => is
148
191
  <template #icon>
149
192
  <Filter :size="14" weight="BoldDuotone" />
150
193
  </template>
151
- </ToolbarFilterTrigger>
194
+ </ToolbarLabelTrigger>
152
195
  </PopoverTrigger>
153
196
 
154
197
  <PopoverContent
@@ -229,13 +272,19 @@ const nothingToReset = computed(() => Object.keys(draft.value).every((key) => is
229
272
  <!-- Wraps rather than truncates: a label reading "Net 30 days,
230
273
  2% early settlement" is unusable cut off, and the pane cannot
231
274
  grow to fit it without becoming the layout it replaced. -->
232
- <span class="min-w-0 flex-1">{{ option.label }}</span>
275
+ <span class="min-w-0 flex-1">
276
+ {{ option.label }}
277
+ <span v-if="option.description" class="block text-xs text-muted-foreground">{{ option.description }}</span>
278
+ </span>
233
279
  <span
234
280
  v-if="option.count !== void 0"
235
281
  class="shrink-0 tabular-nums text-muted-foreground"
236
282
  >{{ option.count }}</span>
237
283
  </label>
238
- <p v-if="!visibleOptions(activeFacet).length" class="px-1.5 py-6 text-center text-sm text-muted-foreground">
284
+ <div v-if="activeFacet.search && remote[activeFacet.key]?.busy && !visibleOptions(activeFacet).length" class="space-y-1.5 p-1">
285
+ <div v-for="i in 4" :key="i" class="h-6 animate-pulse rounded-md bg-muted" />
286
+ </div>
287
+ <p v-else-if="!visibleOptions(activeFacet).length" class="px-1.5 py-6 text-center text-sm text-muted-foreground">
239
288
  {{ query ? t("shared.facetpanel.empty.noMatch", { query: search }) : t("shared.facetpanel.empty.noOption") }}
240
289
  </p>
241
290
  </div>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { Filter } from "#solar-icons";
3
- import { Popover, PopoverContent, PopoverTrigger, ToolbarFilterTrigger } from "@revenexx/studio";
3
+ import { Popover, PopoverContent, PopoverTrigger } from "@revenexx/studio";
4
+ import ToolbarLabelTrigger from "./ToolbarLabelTrigger.vue";
4
5
  defineProps({
5
6
  width: { type: String, required: false, default: "w-72" },
6
7
  active: { type: Boolean, required: false, default: false },
@@ -12,16 +13,15 @@ const t = useT();
12
13
  <template>
13
14
  <Popover>
14
15
  <PopoverTrigger as-child>
15
- <ToolbarFilterTrigger
16
- icon-only
16
+ <ToolbarLabelTrigger
17
17
  :active="active"
18
18
  :count="count"
19
- :label="t('shared.datatable.filtermenu.aria')"
19
+ :label="t('shared.datatable.toolbar.filter.action')"
20
20
  >
21
21
  <template #icon>
22
22
  <Filter :size="14" weight="BoldDuotone" />
23
23
  </template>
24
- </ToolbarFilterTrigger>
24
+ </ToolbarLabelTrigger>
25
25
  </PopoverTrigger>
26
26
 
27
27
  <PopoverContent align="start" :class="['p-0', width]">
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { Bookmark, CheckCircle, Diskette, TrashBinMinimalistic } from "#solar-icons";
2
+ import { Bookmark, CheckCircle, Diskette, TrashBinMinimalistic, UsersGroupRounded } from "#solar-icons";
3
3
  import {
4
4
  Button,
5
5
  Dialog,
@@ -14,7 +14,8 @@ import {
14
14
  DropdownMenuLabel,
15
15
  DropdownMenuSeparator,
16
16
  DropdownMenuTrigger,
17
- Input
17
+ Input,
18
+ Switch
18
19
  } from "@revenexx/studio";
19
20
  import { sameJson } from "../../lib/canonicalJson";
20
21
  const props = defineProps({
@@ -46,11 +47,17 @@ function applyDefault() {
46
47
  function applyView(v) {
47
48
  props.config.apply(v.snapshot);
48
49
  }
50
+ const isMine = (v) => v.mine !== false;
51
+ const myViews = computed(() => views.value.filter(isMine));
52
+ const sharedViews = computed(() => views.value.filter((v) => !isMine(v)));
49
53
  const saveOpen = ref(false);
50
54
  const saveName = ref("");
55
+ const saveShared = ref(false);
51
56
  const saving = ref(false);
52
57
  function openSave() {
53
- saveName.value = appliedView.value?.name ?? "";
58
+ const own = appliedView.value && isMine(appliedView.value) ? appliedView.value : null;
59
+ saveName.value = own?.name ?? "";
60
+ saveShared.value = own?.shared === true;
54
61
  saveOpen.value = true;
55
62
  }
56
63
  async function save() {
@@ -58,8 +65,8 @@ async function save() {
58
65
  if (!name || saving.value) return;
59
66
  saving.value = true;
60
67
  try {
61
- const entry = await store.save(props.config.storageKey, name, props.config.snapshot());
62
- const at = views.value.findIndex((v) => v.name === name);
68
+ const entry = await store.save(props.config.storageKey, name, props.config.snapshot(), { shared: saveShared.value });
69
+ const at = views.value.findIndex((v) => v.name === name && isMine(v));
63
70
  if (at >= 0) views.value.splice(at, 1, entry);
64
71
  else views.value = [...views.value, entry].sort((a, b) => a.name.localeCompare(b.name));
65
72
  saveOpen.value = false;
@@ -82,7 +89,8 @@ async function remove() {
82
89
  <DropdownMenuTrigger as-child>
83
90
  <Button
84
91
  :variant="hasAppliedView ? 'default' : 'outline'"
85
- :size="size === 'sm' ? 'icon-sm' : 'icon'"
92
+ :size="size"
93
+ class="size-8 gap-1.5 px-0"
86
94
  :data-active="hasAppliedView"
87
95
  :aria-label="t('shared.datatable.savedviews.trigger.aria', { name: activeLabel })"
88
96
  :title="t('shared.datatable.savedviews.trigger.aria', { name: activeLabel })"
@@ -97,6 +105,8 @@ async function remove() {
97
105
  <span class="flex shrink-0 items-center" :class="hasAppliedView ? '' : 'text-primary'">
98
106
  <Bookmark :class="size === 'sm' ? 'size-3.5' : 'size-4'" weight="BoldDuotone" />
99
107
  </span>
108
+ <!-- Icon only, the word for screen readers (Michael 12.10.). -->
109
+ <span class="sr-only">{{ t("shared.datatable.savedviews.heading") }}</span>
100
110
  </Button>
101
111
  </DropdownMenuTrigger>
102
112
  <!--
@@ -105,7 +115,7 @@ async function remove() {
105
115
  market switcher's and the column picker's grew right — a menu that opens
106
116
  the other way is one a reader has to re-find every time.
107
117
  -->
108
- <DropdownMenuContent align="start" class="w-60">
118
+ <DropdownMenuContent align="start" class="w-80">
109
119
  <!-- Named, like the market switcher's "Showing" and the column picker's
110
120
  "Columns": a menu of bare rows does not say what it is a menu OF. -->
111
121
  <DropdownMenuLabel>{{ t("shared.datatable.savedviews.heading") }}</DropdownMenuLabel>
@@ -118,16 +128,27 @@ async function remove() {
118
128
  <span class="flex-1 truncate" :class="isDefault ? 'font-medium text-primary' : ''">{{ allLabel }}</span>
119
129
  <CheckCircle v-if="isDefault" :size="14" weight="Bold" class="shrink-0 text-primary" />
120
130
  </DropdownMenuItem>
121
- <template v-if="views.length">
131
+ <template v-if="myViews.length">
122
132
  <DropdownMenuSeparator />
133
+ <DropdownMenuLabel v-if="sharedViews.length" class="text-xs font-medium text-muted-foreground">
134
+ {{ t("shared.datatable.savedviews.group.mine") }}
135
+ </DropdownMenuLabel>
123
136
  <DropdownMenuItem
124
- v-for="v in views"
137
+ v-for="v in myViews"
125
138
  :key="v.id"
126
139
  class="gap-2"
127
140
  :aria-current="isActive(v) ? 'true' : void 0"
128
141
  @select="applyView(v)"
129
142
  >
130
143
  <span class="min-w-0 flex-1 truncate" :class="isActive(v) ? 'font-medium text-primary' : ''">{{ v.name }}</span>
144
+ <span
145
+ v-if="v.shared"
146
+ class="inline-flex shrink-0 items-center gap-0.5 rounded border border-primary/20 bg-primary/[0.06] px-1 text-[10.5px] font-medium text-primary"
147
+ :title="t('shared.datatable.savedviews.savedialog.field.shared.label')"
148
+ >
149
+ <UsersGroupRounded :size="11" weight="Bold" />
150
+ {{ t("shared.datatable.savedviews.badge.shared") }}
151
+ </span>
131
152
  <CheckCircle v-if="isActive(v)" :size="14" weight="Bold" class="shrink-0 text-primary" />
132
153
  <span
133
154
  role="button"
@@ -140,6 +161,26 @@ async function remove() {
140
161
  </span>
141
162
  </DropdownMenuItem>
142
163
  </template>
164
+ <template v-if="sharedViews.length">
165
+ <DropdownMenuSeparator />
166
+ <DropdownMenuLabel class="text-xs font-medium text-muted-foreground">
167
+ {{ t("shared.datatable.savedviews.group.shared") }}
168
+ </DropdownMenuLabel>
169
+ <DropdownMenuItem
170
+ v-for="v in sharedViews"
171
+ :key="v.id"
172
+ class="gap-2"
173
+ :aria-current="isActive(v) ? 'true' : void 0"
174
+ @select="applyView(v)"
175
+ >
176
+ <UsersGroupRounded :size="13" weight="Bold" class="shrink-0 text-primary/70" />
177
+ <span class="min-w-0 flex-1 truncate" :class="isActive(v) ? 'font-medium text-primary' : ''">{{ v.name }}</span>
178
+ <span v-if="v.ownerName" class="max-w-24 shrink-0 truncate text-xs text-muted-foreground">
179
+ {{ t("shared.datatable.savedviews.owner", { name: v.ownerName }) }}
180
+ </span>
181
+ <CheckCircle v-if="isActive(v)" :size="14" weight="Bold" class="shrink-0 text-primary" />
182
+ </DropdownMenuItem>
183
+ </template>
143
184
  <DropdownMenuSeparator />
144
185
  <DropdownMenuItem class="gap-2" @select="openSave">
145
186
  <Diskette :size="14" class="shrink-0 text-muted-foreground" />
@@ -160,7 +201,7 @@ async function remove() {
160
201
  <Dialog :open="saveOpen" @update:open="(v2) => {
161
202
  if (!v2) saveOpen = false;
162
203
  }">
163
- <DialogContent>
204
+ <DialogContent class="sm:max-w-xl">
164
205
  <DialogHeader>
165
206
  <DialogTitle>{{ t("shared.datatable.savedviews.savedialog.title") }}</DialogTitle>
166
207
  <DialogDescription>{{ t("shared.datatable.savedviews.savedialog.description") }}</DialogDescription>
@@ -174,6 +215,14 @@ async function remove() {
174
215
  @keydown.enter="save"
175
216
  />
176
217
  </div>
218
+ <!-- PE-1052: private by default; the owner may share it with the tenant. -->
219
+ <div class="flex items-start justify-between gap-4 rounded-md border border-black/[0.06] px-3 py-2.5">
220
+ <div class="min-w-0">
221
+ <label for="saved-view-shared" class="text-sm font-medium">{{ t("shared.datatable.savedviews.savedialog.field.shared.label") }}</label>
222
+ <p class="mt-0.5 text-xs text-muted-foreground">{{ t("shared.datatable.savedviews.savedialog.field.shared.hint") }}</p>
223
+ </div>
224
+ <Switch id="saved-view-shared" v-model="saveShared" class="mt-0.5 shrink-0" />
225
+ </div>
177
226
  <DialogFooter>
178
227
  <Button variant="outline" @click="saveOpen = false">
179
228
  {{ t("shared.datatable.savedviews.savedialog.action.cancel") }}
@@ -189,9 +238,9 @@ async function remove() {
189
238
  <Dialog :open="deleteTarget !== null" @update:open="(v2) => {
190
239
  if (!v2) deleteTarget = null;
191
240
  }">
192
- <DialogContent>
241
+ <DialogContent class="sm:max-w-xl">
193
242
  <DialogHeader>
194
- <DialogTitle>{{ t("shared.datatable.savedviews.deletedialog.title") }}</DialogTitle>
243
+ <DialogTitle>{{ t("shared.datatable.savedviews.deletedialog.title", { name: deleteTarget?.name ?? "" }) }}</DialogTitle>
195
244
  <DialogDescription>
196
245
  {{ t("shared.datatable.savedviews.deletedialog.description", { name: deleteTarget?.name ?? "" }) }}
197
246
  </DialogDescription>
@@ -37,7 +37,8 @@ function pick(key, direction) {
37
37
  <DropdownMenuTrigger as-child>
38
38
  <Button
39
39
  :variant="sorted ? 'default' : 'outline'"
40
- :size="size === 'sm' ? 'icon-sm' : 'icon'"
40
+ :size="size"
41
+ class="size-8 gap-1.5 px-0"
41
42
  :data-active="sorted"
42
43
  :aria-label="t('shared.datatable.sortmenu.aria', { sort: label })"
43
44
  :title="t('shared.datatable.sortmenu.aria', { sort: label })"
@@ -48,6 +49,8 @@ function pick(key, direction) {
48
49
  <span class="flex shrink-0 items-center" :class="sorted ? '' : 'text-primary'">
49
50
  <component :is="icon" weight="Linear" :class="size === 'sm' ? 'size-3.5' : 'size-4'" />
50
51
  </span>
52
+ <!-- Icon only, the word for screen readers (Michael 12.10.). -->
53
+ <span class="sr-only">{{ t("shared.datatable.sortmenu.trigger") }}</span>
51
54
  </Button>
52
55
  </DropdownMenuTrigger>
53
56
  <DropdownMenuContent align="start" class="w-56">
@@ -0,0 +1,23 @@
1
+ type __VLS_Props = {
2
+ label: string;
3
+ active?: boolean;
4
+ count?: number;
5
+ size?: 'sm' | 'default';
6
+ };
7
+ declare var __VLS_8: {};
8
+ type __VLS_Slots = {} & {
9
+ icon?: (props: typeof __VLS_8) => any;
10
+ };
11
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
12
+ size: "sm" | "default";
13
+ count: number;
14
+ active: boolean;
15
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
17
+ declare const _default: typeof __VLS_export;
18
+ export default _default;
19
+ type __VLS_WithSlots<T, S> = T & {
20
+ new (): {
21
+ $slots: S;
22
+ };
23
+ };
@@ -0,0 +1,29 @@
1
+ <script setup>
2
+ import { Button } from "@revenexx/studio";
3
+ const props = defineProps({
4
+ label: { type: String, required: true },
5
+ active: { type: Boolean, required: false, default: false },
6
+ count: { type: Number, required: false, default: 0 },
7
+ size: { type: String, required: false, default: "sm" }
8
+ });
9
+ </script>
10
+
11
+ <template>
12
+ <Button
13
+ :variant="props.active ? 'default' : 'outline'"
14
+ :size="props.size"
15
+ class="relative size-8 gap-1.5 px-0"
16
+ :data-active="props.active"
17
+ :aria-label="props.label"
18
+ :title="props.label"
19
+ >
20
+ <span class="flex shrink-0 items-center" :class="props.active ? '' : 'text-primary'">
21
+ <slot name="icon" />
22
+ </span>
23
+ <span class="sr-only">{{ props.label }}</span>
24
+ <span
25
+ v-if="props.active && props.count > 0"
26
+ class="flex h-4 min-w-4 items-center justify-center rounded-full bg-primary-foreground px-1 text-[0.625rem]/none font-semibold tabular-nums text-primary absolute -top-1.5 -right-1.5 bg-primary text-primary-foreground ring-2 ring-card"
27
+ >{{ props.count }}</span>
28
+ </Button>
29
+ </template>
@@ -0,0 +1,23 @@
1
+ type __VLS_Props = {
2
+ label: string;
3
+ active?: boolean;
4
+ count?: number;
5
+ size?: 'sm' | 'default';
6
+ };
7
+ declare var __VLS_8: {};
8
+ type __VLS_Slots = {} & {
9
+ icon?: (props: typeof __VLS_8) => any;
10
+ };
11
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
12
+ size: "sm" | "default";
13
+ count: number;
14
+ active: boolean;
15
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
17
+ declare const _default: typeof __VLS_export;
18
+ export default _default;
19
+ type __VLS_WithSlots<T, S> = T & {
20
+ new (): {
21
+ $slots: S;
22
+ };
23
+ };
@@ -25,6 +25,13 @@ export interface DataTableColumn<T = unknown> {
25
25
  alignRight?: boolean;
26
26
  /** Optional icon component for the header. */
27
27
  icon?: Component;
28
+ /** The narrowest this column may get when it takes a share (`fr`) of the
29
+ * width — for a name or company column that must stay readable while ID
30
+ * columns size to their content. The `primary` column has its own floor. */
31
+ floor?: string;
32
+ /** Keeps a trailing column (the row's „…“ menu) in view while the table
33
+ * scrolls sideways, like the identifying column on the left. */
34
+ pinRight?: boolean;
28
35
  /** Marks the identifying column. When set (and no `#cell-<key>` slot is
29
36
  * given) the DataTable renders the value as the bold record anchor —
30
37
  * `font-semibold text-[#0B0522]`, truncated — optionally led by a tinted
@@ -36,7 +43,17 @@ export interface DataTableColumn<T = unknown> {
36
43
  * `cellIcon` must NOT be a getter: Solar icons are functional components, so
37
44
  * the table can't tell a getter from an icon and would crash invoking it.) */
38
45
  cellIcon?: Component;
46
+ /**
47
+ * The column's part in the phone layout, where each row is drawn as a card
48
+ * (R4, Charge 2). `title` heads the card (defaults to the `primary` column),
49
+ * `status` sits beside it as the badge, `meta` columns fill the two lines of
50
+ * key facts in declared order, `hidden` leaves the column out. Unset: the
51
+ * table decides from `primary` and the column key.
52
+ */
53
+ card?: 'title' | 'status' | 'meta' | 'hidden';
39
54
  }
55
+ /** Row height (D4, Charge 2): `standard` 44px, `compact` 36px. */
56
+ export type DataTableDensity = 'standard' | 'compact';
40
57
  /** A built-in filter dimension rendered under the generic "Filter" dropdown as
41
58
  * a labelled sub-menu (datatable-ux). Static mode only: the DataTable filters
42
59
  * `data` in-memory and shows a count per option. Pass one, or an array of
@@ -4,7 +4,7 @@
4
4
 
5
5
  <template>
6
6
  <div class="@container">
7
- <div class="grid grid-cols-1 gap-x-4 gap-y-3.5 @md:grid-cols-2">
7
+ <div class="grid grid-cols-1 gap-x-4 gap-y-3.5 @lg:grid-cols-2">
8
8
  <slot />
9
9
  </div>
10
10
  </div>
@@ -29,8 +29,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
29
29
  onCancel?: (() => any) | undefined;
30
30
  }>, {
31
31
  dirty: boolean;
32
- canSubmit: boolean;
33
32
  busy: boolean;
33
+ canSubmit: boolean;
34
34
  allowClean: boolean;
35
35
  hideCancel: boolean;
36
36
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;