@revenexx/studio-shared 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +22 -12
  2. package/dist/module.d.mts +15 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +58 -0
  5. package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +12 -0
  6. package/{components → dist/runtime/components}/DataTable/ColumnsMenu.vue +13 -19
  7. package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +12 -0
  8. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +87 -0
  9. package/dist/runtime/components/DataTable/DataTable.vue +510 -0
  10. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +87 -0
  11. package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +21 -0
  12. package/dist/runtime/components/DataTable/FilterMenu.vue +46 -0
  13. package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +21 -0
  14. package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -0
  15. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +189 -0
  16. package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -0
  17. package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +13 -0
  18. package/dist/runtime/components/DataTable/SortMenu.vue +47 -0
  19. package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +13 -0
  20. package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +20 -0
  21. package/dist/runtime/components/DataTable/ViewModeSwitch.vue +26 -0
  22. package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +20 -0
  23. package/dist/runtime/components/DataTable/types.d.ts +104 -0
  24. package/dist/runtime/components/DataTable/types.js +0 -0
  25. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +17 -0
  26. package/dist/runtime/components/Media/MediaFolderTree.vue +65 -0
  27. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +17 -0
  28. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +18 -0
  29. package/dist/runtime/components/Media/MediaPickerDialog.vue +163 -0
  30. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +18 -0
  31. package/dist/runtime/components/Media/MediaUploader.d.vue.ts +29 -0
  32. package/dist/runtime/components/Media/MediaUploader.vue +161 -0
  33. package/dist/runtime/components/Media/MediaUploader.vue.d.ts +29 -0
  34. package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +30 -0
  35. package/{components → dist/runtime/components}/Shared/BuildLogsFlyout.vue +37 -54
  36. package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +30 -0
  37. package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +17 -0
  38. package/dist/runtime/components/Shared/GitDeploymentSettings.vue +75 -0
  39. package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +17 -0
  40. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +13 -0
  41. package/{components → dist/runtime/components}/Shared/KpiTile.vue +11 -19
  42. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +13 -0
  43. package/dist/runtime/components/Shared/ListSurface.d.vue.ts +38 -0
  44. package/dist/runtime/components/Shared/ListSurface.vue +27 -0
  45. package/dist/runtime/components/Shared/ListSurface.vue.d.ts +38 -0
  46. package/dist/runtime/components/Shared/PageHeader.d.vue.ts +23 -0
  47. package/{components → dist/runtime/components}/Shared/PageHeader.vue +6 -8
  48. package/dist/runtime/components/Shared/PageHeader.vue.d.ts +23 -0
  49. package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +52 -0
  50. package/dist/runtime/components/Shared/RecordLayout.vue +63 -0
  51. package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +52 -0
  52. package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +26 -0
  53. package/dist/runtime/components/Shared/RecordRailCard.vue +20 -0
  54. package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +26 -0
  55. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +41 -0
  56. package/dist/runtime/components/Shared/SidebarNav.vue +48 -0
  57. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +41 -0
  58. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +15 -0
  59. package/dist/runtime/components/Shared/SidebarNavLink.vue +49 -0
  60. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +15 -0
  61. package/dist/runtime/components/Shared/SortHeader.d.vue.ts +23 -0
  62. package/dist/runtime/components/Shared/SortHeader.vue +35 -0
  63. package/dist/runtime/components/Shared/SortHeader.vue.d.ts +23 -0
  64. package/dist/runtime/components/Shared/StateCard.d.vue.ts +32 -0
  65. package/{components → dist/runtime/components}/Shared/StateCard.vue +21 -36
  66. package/dist/runtime/components/Shared/StateCard.vue.d.ts +32 -0
  67. package/dist/runtime/components/Shared/StudioCard.d.vue.ts +15 -0
  68. package/dist/runtime/components/Shared/StudioCard.vue +19 -0
  69. package/dist/runtime/components/Shared/StudioCard.vue.d.ts +15 -0
  70. package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -0
  71. package/{components → dist/runtime/components}/Shared/TabBar.vue +7 -16
  72. package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -0
  73. package/dist/runtime/components/Shared/ViewTab.d.vue.ts +7 -0
  74. package/{components → dist/runtime/components}/Shared/ViewTab.vue +7 -9
  75. package/dist/runtime/components/Shared/ViewTab.vue.d.ts +7 -0
  76. package/dist/runtime/components/Shared/Wizard.d.vue.ts +47 -0
  77. package/dist/runtime/components/Shared/Wizard.vue +66 -0
  78. package/dist/runtime/components/Shared/Wizard.vue.d.ts +47 -0
  79. package/dist/runtime/composables/useListRail.d.ts +9 -0
  80. package/dist/runtime/composables/useListRail.js +19 -0
  81. package/dist/runtime/composables/useMediaLibrary.d.ts +41 -0
  82. package/dist/runtime/composables/useMediaLibrary.js +36 -0
  83. package/dist/runtime/composables/usePlatformAuth.d.ts +35 -0
  84. package/dist/runtime/composables/usePlatformAuth.js +17 -0
  85. package/dist/runtime/composables/usePlatformTalkback.d.ts +29 -0
  86. package/dist/runtime/composables/usePlatformTalkback.js +54 -0
  87. package/dist/runtime/composables/usePlatformTenant.d.ts +14 -0
  88. package/dist/runtime/composables/usePlatformTenant.js +18 -0
  89. package/dist/runtime/composables/useT.d.ts +11 -0
  90. package/dist/runtime/composables/useT.js +8 -0
  91. package/dist/runtime/composables/useTalkbackChannels.d.ts +78 -0
  92. package/dist/runtime/composables/useTalkbackChannels.js +52 -0
  93. package/dist/runtime/lib/formatBridge.d.ts +50 -0
  94. package/dist/runtime/lib/formatBridge.js +54 -0
  95. package/dist/runtime/lib/i18nBridge.d.ts +31 -0
  96. package/dist/runtime/lib/i18nBridge.js +17 -0
  97. package/dist/runtime/lib/isoLists.d.ts +61 -0
  98. package/dist/runtime/lib/isoLists.js +62 -0
  99. package/dist/runtime/lib/sidebarNav.d.ts +34 -0
  100. package/dist/runtime/lib/sidebarNav.js +22 -0
  101. package/dist/runtime/lib/talkback.d.ts +59 -0
  102. package/dist/runtime/lib/talkback.js +49 -0
  103. package/dist/runtime/types/nav.d.ts +37 -0
  104. package/dist/runtime/types/nav.js +0 -0
  105. package/dist/runtime/types/storage.d.ts +67 -0
  106. package/dist/runtime/types/storage.js +0 -0
  107. package/dist/types.d.mts +27 -0
  108. package/package.json +41 -13
  109. package/components/DataTable/DataTable.vue +0 -518
  110. package/components/DataTable/FilterMenu.vue +0 -40
  111. package/components/DataTable/types.ts +0 -82
  112. package/components/Shared/GitDeploymentSettings.vue +0 -87
  113. package/components/Shared/SidebarNav.vue +0 -100
  114. package/components/Shared/SortHeader.vue +0 -50
  115. package/components/Shared/StudioCard.vue +0 -24
  116. package/components/Shared/Wizard.vue +0 -80
  117. package/composables/usePlatformAuth.ts +0 -35
  118. package/composables/usePlatformTenant.ts +0 -35
  119. package/index.ts +0 -16
  120. package/nuxt.config.ts +0 -23
@@ -0,0 +1,510 @@
1
+ <script setup>
2
+ import { AltArrowDown, Filter, Magnifier as Search, SidebarMinimalistic } from "#solar-icons";
3
+ import {
4
+ Button,
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuSub,
10
+ DropdownMenuSubContent,
11
+ DropdownMenuSubTrigger,
12
+ DropdownMenuTrigger,
13
+ Input
14
+ } from "@revenexx/studio";
15
+ import SortHeader from "../Shared/SortHeader.vue";
16
+ import ColumnsMenu from "./ColumnsMenu.vue";
17
+ import FilterMenu from "./FilterMenu.vue";
18
+ import SavedViewsMenu from "./SavedViewsMenu.vue";
19
+ import SortMenu from "./SortMenu.vue";
20
+ import ViewModeSwitch from "./ViewModeSwitch.vue";
21
+ const props = defineProps({
22
+ data: { type: Array, required: true },
23
+ columns: { type: Array, required: true },
24
+ rowKey: { type: Function, required: true, skipCheck: true },
25
+ search: { type: [Boolean, Object], required: false, default: true },
26
+ columnsMenu: { type: Boolean, required: false, default: true },
27
+ filter: { type: [Boolean, Object], required: false, default: false },
28
+ filterTabs: { type: [Object, Array], required: false },
29
+ savedViews: { type: [Object, null], required: false, default: null },
30
+ viewModes: { type: [Array, null], required: false, default: null },
31
+ rail: { type: [Boolean, Object], required: false, default: false },
32
+ pagination: { type: [Boolean, Object], required: false, default: true },
33
+ defaultSort: { type: Object, required: false },
34
+ serverMode: { type: Boolean, required: false, default: false },
35
+ total: { type: Number, required: false },
36
+ loading: { type: Boolean, required: false, default: false },
37
+ emptyText: { type: String, required: false },
38
+ groupBy: { type: [Function, null], required: false, default: null },
39
+ groupEmptyLabel: { type: String, required: false }
40
+ });
41
+ const emit = defineEmits(["rowClick", "stateChange"]);
42
+ const t = useT();
43
+ const query = ref("");
44
+ const sort = defineModel("sort", { type: Object, ...{
45
+ default: () => ({
46
+ column: null,
47
+ direction: null
48
+ })
49
+ } });
50
+ const page = defineModel("page", { type: Number, ...{ default: 0 } });
51
+ const viewMode = defineModel("viewMode", { type: String, ...{ default: "list" } });
52
+ const railOpen = defineModel("railOpen", { type: Boolean, ...{ default: true } });
53
+ if (props.defaultSort && sort.value.column == null) {
54
+ sort.value = { column: props.defaultSort.column, direction: props.defaultSort.direction };
55
+ }
56
+ const pageSize = ref(
57
+ typeof props.pagination === "object" && props.pagination?.pageSize ? props.pagination.pageSize : 20
58
+ );
59
+ const pageSizeOptions = computed(() => Array.from(/* @__PURE__ */ new Set([10, 20, 50, 100, pageSize.value])).sort((a, b) => a - b));
60
+ const visibleKeys = ref(
61
+ new Set(props.columns.filter((c) => c.visible !== false).map((c) => c.key))
62
+ );
63
+ function toggleColumn(key) {
64
+ const next = new Set(visibleKeys.value);
65
+ if (next.has(key)) next.delete(key);
66
+ else next.add(key);
67
+ visibleKeys.value = next;
68
+ }
69
+ const visibleColumns = computed(
70
+ () => props.columns.filter((c) => visibleKeys.value.has(c.key))
71
+ );
72
+ const menuColumns = computed(() => props.columns);
73
+ function searchOf(col, row) {
74
+ if (col.searchGetter) return col.searchGetter(row).toLowerCase();
75
+ if (col.sortGetter) {
76
+ const v = col.sortGetter(row);
77
+ return v == null ? "" : String(v).toLowerCase();
78
+ }
79
+ const raw = row[col.key];
80
+ return raw == null ? "" : String(raw).toLowerCase();
81
+ }
82
+ const filterDims = computed(() => {
83
+ const raw = props.filterTabs;
84
+ if (!raw) return [];
85
+ const arr = Array.isArray(raw) ? raw : [raw];
86
+ return arr.map((f, i) => ({ key: f.key ?? f.label ?? `dim${i}`, label: f.label ?? t("shared.datatable.filter.dimension.label.default"), getter: f.getter, options: f.options, allLabel: f.allLabel }));
87
+ });
88
+ const activeFilters = ref({});
89
+ const anyFilterActive = computed(() => Object.values(activeFilters.value).some(Boolean));
90
+ function setFilter(key, value) {
91
+ activeFilters.value = { ...activeFilters.value, [key]: value };
92
+ }
93
+ function dimCounts(d) {
94
+ const m = {};
95
+ for (const row of props.data) {
96
+ const k = String(d.getter(row) ?? "");
97
+ m[k] = (m[k] ?? 0) + 1;
98
+ }
99
+ return m;
100
+ }
101
+ const filtered = computed(() => {
102
+ if (props.serverMode || !filterDims.value.length) return props.data;
103
+ const active = filterDims.value.filter((d) => activeFilters.value[d.key]);
104
+ if (!active.length) return props.data;
105
+ return props.data.filter((row) => active.every((d) => String(d.getter(row) ?? "") === activeFilters.value[d.key]));
106
+ });
107
+ const searched = computed(() => {
108
+ if (props.serverMode) return props.data;
109
+ const q = query.value.trim().toLowerCase();
110
+ if (!q) return filtered.value;
111
+ return filtered.value.filter(
112
+ (row) => props.columns.some((col) => searchOf(col, row).includes(q))
113
+ );
114
+ });
115
+ function sortKey(col, row) {
116
+ if (col.sortGetter) return col.sortGetter(row);
117
+ const raw = row[col.key];
118
+ return raw;
119
+ }
120
+ const sorted = computed(() => {
121
+ if (props.serverMode) return searched.value;
122
+ const col = sort.value.column;
123
+ const dir = sort.value.direction;
124
+ if (!col || !dir) return searched.value;
125
+ const c = props.columns.find((x) => x.key === col);
126
+ if (!c) return searched.value;
127
+ const sign = dir === "asc" ? 1 : -1;
128
+ return [...searched.value].sort((a, b) => {
129
+ const av = sortKey(c, a);
130
+ const bv = sortKey(c, b);
131
+ const aNull = av == null || av === "";
132
+ const bNull = bv == null || bv === "";
133
+ if (aNull && bNull) return 0;
134
+ if (aNull) return 1;
135
+ if (bNull) return -1;
136
+ const aCmp = typeof av === "boolean" ? av ? 1 : 0 : av;
137
+ const bCmp = typeof bv === "boolean" ? bv ? 1 : 0 : bv;
138
+ if (aCmp < bCmp) return -1 * sign;
139
+ if (aCmp > bCmp) return 1 * sign;
140
+ return 0;
141
+ });
142
+ });
143
+ const totalCount = computed(
144
+ () => props.serverMode ? props.total ?? props.data.length : sorted.value.length
145
+ );
146
+ const paginated = computed(() => {
147
+ if (props.serverMode || !props.pagination) return sorted.value;
148
+ const start = page.value * pageSize.value;
149
+ return sorted.value.slice(start, start + pageSize.value);
150
+ });
151
+ const displayedRows = computed(() => props.groupBy ? sorted.value : paginated.value);
152
+ const keyedRows = computed(() => {
153
+ const seen = /* @__PURE__ */ new Map();
154
+ return displayedRows.value.map((row) => {
155
+ const base = rowKeyFor(row) ?? "\u2205";
156
+ const n = seen.get(base) ?? 0;
157
+ seen.set(base, n + 1);
158
+ return { row, key: n === 0 ? base : `${base}#${n}` };
159
+ });
160
+ });
161
+ const renderRows = computed(() => {
162
+ if (!props.groupBy) return keyedRows.value.map(({ row, key }) => ({ kind: "row", row, key }));
163
+ const emptyLabel = props.groupEmptyLabel ?? t("shared.datatable.group.ungrouped.default");
164
+ const groups = /* @__PURE__ */ new Map();
165
+ for (const entry of keyedRows.value) {
166
+ const labels2 = props.groupBy(entry.row);
167
+ for (const label of labels2.length ? labels2 : [emptyLabel]) {
168
+ if (!groups.has(label)) groups.set(label, []);
169
+ groups.get(label).push(entry);
170
+ }
171
+ }
172
+ const labels = [...groups.keys()].sort((a, b) => a.localeCompare(b));
173
+ if (labels.includes(emptyLabel)) {
174
+ labels.splice(labels.indexOf(emptyLabel), 1);
175
+ labels.push(emptyLabel);
176
+ }
177
+ const out = [];
178
+ for (const label of labels) {
179
+ const rows = groups.get(label);
180
+ out.push({ kind: "header", label, count: rows.length, key: `__group__${label}` });
181
+ for (const e of rows) out.push({ kind: "row", row: e.row, key: `${label}::${e.key}` });
182
+ }
183
+ return out;
184
+ });
185
+ function emitState() {
186
+ emit("stateChange", {
187
+ search: query.value,
188
+ sort: { ...sort.value },
189
+ page: page.value,
190
+ pageSize: pageSize.value
191
+ });
192
+ }
193
+ watch([query, sort, page, pageSize], () => {
194
+ if (props.serverMode) emitState();
195
+ });
196
+ watch([query, sort, pageSize, activeFilters], () => {
197
+ page.value = 0;
198
+ }, { deep: true });
199
+ function toggleSort(column) {
200
+ if (sort.value.column !== column) {
201
+ sort.value = { column, direction: "asc" };
202
+ return;
203
+ }
204
+ if (sort.value.direction === "asc") {
205
+ sort.value = { column, direction: "desc" };
206
+ return;
207
+ }
208
+ sort.value = { column: null, direction: null };
209
+ }
210
+ function rowKeyFor(row) {
211
+ if (typeof props.rowKey === "function") return props.rowKey(row);
212
+ return row[props.rowKey];
213
+ }
214
+ const gridTemplate = computed(() => {
215
+ const tracks = visibleColumns.value.map((c) => c.width ?? "minmax(0,1fr)");
216
+ return `grid-template-columns: ${tracks.join(" ")};`;
217
+ });
218
+ const placeholder = computed(
219
+ () => typeof props.search === "object" ? props.search.placeholder ?? t("shared.datatable.toolbar.search.placeholder.default") : t("shared.datatable.toolbar.search.placeholder.default")
220
+ );
221
+ const slots = useSlots();
222
+ const showToolbar = computed(
223
+ () => props.search || props.columnsMenu || props.filter || !!props.filterTabs || !!props.savedViews || !!props.viewModes?.length || !!props.rail || !!slots.actions || !!slots.scope || !!slots["toolbar-left"] || !!slots["toolbar-right"]
224
+ );
225
+ const railLabel = computed(
226
+ () => (typeof props.rail === "object" ? props.rail.label : void 0) ?? t("shared.datatable.toolbar.rail.label.default")
227
+ );
228
+ const showTable = computed(() => !props.viewModes?.length || viewMode.value === "list");
229
+ const sortableColumns = computed(() => props.columns.filter((c) => c.sortable));
230
+ const showSortMenu = computed(() => !showTable.value && sortableColumns.value.length > 0);
231
+ function cellIconFor(col) {
232
+ return col.cellIcon ?? null;
233
+ }
234
+ const showFooter = computed(() => props.pagination !== false && !props.groupBy);
235
+ const lastPage = computed(
236
+ () => pageSize.value > 0 ? Math.max(0, Math.ceil(totalCount.value / pageSize.value) - 1) : 0
237
+ );
238
+ const rangeStart = computed(() => totalCount.value === 0 ? 0 : page.value * pageSize.value + 1);
239
+ const rangeEnd = computed(
240
+ () => Math.min((page.value + 1) * pageSize.value, totalCount.value)
241
+ );
242
+ function prevPage() {
243
+ if (page.value > 0) page.value -= 1;
244
+ }
245
+ function nextPage() {
246
+ if (page.value < lastPage.value) page.value += 1;
247
+ }
248
+ function defaultCell(col, row) {
249
+ const v = col.sortGetter ? col.sortGetter(row) : row[col.key];
250
+ if (v == null) return "\u2014";
251
+ return String(v);
252
+ }
253
+ </script>
254
+
255
+ <template>
256
+ <div class="rounded-lg border border-black/[0.06] overflow-hidden flex flex-col">
257
+ <!-- Toolbar ------------------------------------------------------ -->
258
+ <!-- The single control row of a list surface:
259
+ LEFT = rail · search · filter · columns · #toolbar-left
260
+ RIGHT = #toolbar-right · saved views · #scope · view mode · #actions
261
+ (Pagination always lives in the footer.) -->
262
+ <div
263
+ v-if="showToolbar"
264
+ class="flex flex-wrap items-center justify-between gap-2 px-4 py-3 bg-white/55 backdrop-blur-md"
265
+ >
266
+ <div class="flex items-center gap-2 min-w-0">
267
+ <Button
268
+ v-if="rail"
269
+ variant="outline"
270
+ size="sm"
271
+ class="h-7 shrink-0 gap-1.5 text-sm"
272
+ :class="railOpen ? 'border-primary/30 text-primary' : ''"
273
+ :aria-pressed="railOpen"
274
+ :aria-label="railOpen ? t('shared.datatable.toolbar.rail.aria.hide', { label: railLabel }) : t('shared.datatable.toolbar.rail.aria.show', { label: railLabel })"
275
+ @click="railOpen = !railOpen"
276
+ >
277
+ <SidebarMinimalistic :size="14" />
278
+ {{ railLabel }}
279
+ </Button>
280
+ <!-- Shrinks (never wraps the row) once the toolbar carries scope /
281
+ view-mode / saved-view controls next to it. -->
282
+ <div v-if="search" class="relative w-[21rem] min-w-40 max-w-full shrink">
283
+ <Search :size="13" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
284
+ <Input
285
+ v-model="query"
286
+ :placeholder="placeholder"
287
+ class="h-7 pl-8 text-sm bg-white"
288
+ />
289
+ </div>
290
+ <!-- Generic "Filter" dropdown — each dimension is a labelled sub-menu. -->
291
+ <DropdownMenu v-if="filterDims.length">
292
+ <DropdownMenuTrigger as-child>
293
+ <Button
294
+ variant="outline"
295
+ size="sm"
296
+ class="h-7 gap-1.5 text-sm"
297
+ :class="anyFilterActive ? 'border-primary/30 text-primary' : ''"
298
+ >
299
+ <Filter :size="14" />
300
+ {{ t("shared.datatable.toolbar.filter.action") }}
301
+ <AltArrowDown :size="12" class="opacity-60" />
302
+ </Button>
303
+ </DropdownMenuTrigger>
304
+ <DropdownMenuContent align="start" class="w-48">
305
+ <DropdownMenuSub v-for="d in filterDims" :key="d.key">
306
+ <DropdownMenuSubTrigger class="gap-2">
307
+ <span class="flex-1">{{ d.label }}</span>
308
+ <span v-if="activeFilters[d.key]" class="text-xs text-primary capitalize">{{ activeFilters[d.key] }}</span>
309
+ </DropdownMenuSubTrigger>
310
+ <DropdownMenuSubContent class="w-48">
311
+ <DropdownMenuItem class="gap-2" @select="setFilter(d.key, null)">
312
+ <span class="flex-1">{{ d.allLabel ?? t("shared.datatable.toolbar.filter.all.default") }}</span>
313
+ <span class="tabular-nums text-muted-foreground">{{ data.length }}</span>
314
+ </DropdownMenuItem>
315
+ <DropdownMenuSeparator />
316
+ <DropdownMenuItem
317
+ v-for="o in d.options"
318
+ :key="o.value"
319
+ class="gap-2"
320
+ @select="setFilter(d.key, o.value)"
321
+ >
322
+ <span class="flex-1">{{ o.label }}</span>
323
+ <span class="tabular-nums text-muted-foreground">{{ dimCounts(d)[o.value] ?? 0 }}</span>
324
+ </DropdownMenuItem>
325
+ </DropdownMenuSubContent>
326
+ </DropdownMenuSub>
327
+ </DropdownMenuContent>
328
+ </DropdownMenu>
329
+ <FilterMenu
330
+ v-if="filter"
331
+ :width="typeof filter === 'object' ? filter.width : void 0"
332
+ :active="typeof filter === 'object' ? filter.active : void 0"
333
+ >
334
+ <slot name="filter-menu" />
335
+ </FilterMenu>
336
+ <SortMenu v-if="showSortMenu" v-model="sort" :columns="sortableColumns" />
337
+ <ColumnsMenu
338
+ v-if="columnsMenu && showTable"
339
+ :columns="menuColumns"
340
+ :visible="visibleKeys"
341
+ @toggle="toggleColumn"
342
+ />
343
+ <slot name="toolbar-left" />
344
+ </div>
345
+ <div class="flex items-center gap-1.5 ml-auto shrink-0">
346
+ <slot name="toolbar-right" />
347
+ <SavedViewsMenu v-if="savedViews" :config="savedViews" />
348
+ <!-- Market scope ("Showing: … / All markets") — the surface passes the
349
+ shared MarketScopeToggle in; the row position is fixed here. -->
350
+ <slot name="scope" />
351
+ <ViewModeSwitch v-if="viewModes?.length" v-model="viewMode" :modes="viewModes" />
352
+ <slot name="actions" />
353
+ </div>
354
+ </div>
355
+
356
+ <!-- Alternate view mode (gallery, board, …) — same toolbar, same footer. -->
357
+ <div v-if="!showTable" class="border-t border-black/[0.04] bg-white/55">
358
+ <slot name="view" :rows="displayedRows" :loading="loading" />
359
+ </div>
360
+
361
+ <!-- Header row -------------------------------------------------- -->
362
+ <div
363
+ v-if="showTable"
364
+ class="grid items-center gap-2 border-t rounded-t-lg border-gray-200 bg-gray-100 px-4 py-2.5 text-sm font-medium uppercase tracking-normal text-muted-foreground"
365
+ :style="gridTemplate"
366
+ >
367
+ <div
368
+ v-for="col in visibleColumns"
369
+ :key="col.key"
370
+ :class="col.alignRight ? 'text-right' : ''"
371
+ >
372
+ <!-- `#header-<key>` lets a page take over a header cell (e.g. a
373
+ select-all checkbox in a selection column). Default: sortable
374
+ SortHeader or the plain label. -->
375
+ <slot :name="`header-${col.key}`">
376
+ <SortHeader
377
+ v-if="col.sortable"
378
+ :column="col.key"
379
+ :active="sort.column"
380
+ :direction="sort.direction"
381
+ :align-right="col.alignRight"
382
+ @toggle="toggleSort"
383
+ >
384
+ {{ col.label }}
385
+ </SortHeader>
386
+ <span v-else>{{ col.label }}</span>
387
+ </slot>
388
+ </div>
389
+ </div>
390
+
391
+ <!-- Body --------------------------------------------------------- -->
392
+ <template v-for="entry in showTable ? renderRows : []" :key="entry.key">
393
+ <!-- Group header (groupBy mode) -->
394
+ <div
395
+ v-if="entry.kind === 'header'"
396
+ class="flex items-center gap-2 border-t border-black/[0.04] bg-gray-100/70 px-4 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
397
+ >
398
+ {{ entry.label }}
399
+ <span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
400
+ </div>
401
+ <div
402
+ v-else
403
+ class="group grid items-center gap-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer bg-gray-50 transition-colors"
404
+ :style="gridTemplate"
405
+ @click="emit('rowClick', entry.row)"
406
+ >
407
+ <div
408
+ v-for="col in visibleColumns"
409
+ :key="col.key"
410
+ class="min-w-0"
411
+ :class="col.alignRight ? 'text-right' : ''"
412
+ >
413
+ <slot :name="`cell-${col.key}`" :row="entry.row">
414
+ <!-- Primary column: bold record anchor + optional tinted icon tile. -->
415
+ <div v-if="col.primary" class="flex items-center gap-2.5 min-w-0">
416
+ <span
417
+ v-if="cellIconFor(col)"
418
+ class="flex size-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"
419
+ >
420
+ <component :is="cellIconFor(col)" :size="13" />
421
+ </span>
422
+ <span class="text-sm font-semibold text-[#0B0522] truncate transition-colors group-hover:text-primary">{{ defaultCell(col, entry.row) }}</span>
423
+ </div>
424
+ <!-- `block`, or `truncate` clips nothing: an inline span establishes
425
+ no box, so a long unbroken value overruns the next column
426
+ instead of ending in an ellipsis. -->
427
+ <span v-else class="block truncate text-sm" :title="String(defaultCell(col, entry.row) ?? '')">{{ defaultCell(col, entry.row) }}</span>
428
+ </slot>
429
+ </div>
430
+ </div>
431
+ </template>
432
+
433
+ <!-- Loading skeleton (only when there are no rows yet) ----------- -->
434
+ <template v-if="showTable && loading && !displayedRows.length">
435
+ <div
436
+ v-for="n in 6"
437
+ :key="`sk-${n}`"
438
+ class="grid items-center gap-2 border-t border-black/[0.04] bg-gray-50 px-4 py-3"
439
+ :style="gridTemplate"
440
+ >
441
+ <div
442
+ v-for="col in visibleColumns"
443
+ :key="col.key"
444
+ :class="col.alignRight ? 'flex justify-end' : ''"
445
+ >
446
+ <div
447
+ class="h-3 rounded bg-black/[0.07] animate-pulse"
448
+ :class="col.alignRight ? 'w-10' : 'w-2/3'"
449
+ />
450
+ </div>
451
+ </div>
452
+ </template>
453
+
454
+ <!-- Empty -------------------------------------------------------- -->
455
+ <div
456
+ v-if="showTable && !displayedRows.length && !loading"
457
+ class="px-3 py-8 text-center text-sm text-muted-foreground"
458
+ >
459
+ <slot name="empty">
460
+ {{ emptyText ?? t("shared.datatable.empty.default") }}
461
+ </slot>
462
+ </div>
463
+
464
+ <!-- Footer ------------------------------------------------------- -->
465
+ <div
466
+ v-if="showFooter"
467
+ class="flex items-center justify-between gap-2 border-t border-gray-200 bg-white/55 backdrop-blur-md px-4 py-3 text-sm text-muted-foreground/60"
468
+ >
469
+ <div class="flex items-center gap-2.5">
470
+ <span class="tabular-nums">
471
+ <span v-if="totalCount === 0">{{ t("shared.datatable.footer.noresults") }}</span>
472
+ <span v-else>{{ t("shared.datatable.footer.showing", { start: rangeStart, end: rangeEnd, total: totalCount }) }}</span>
473
+ </span>
474
+ <span class="text-muted-foreground/40">-</span>
475
+ <label class="flex items-center gap-1.5">
476
+ <span class="hidden sm:inline">{{ t("shared.datatable.footer.pagesize.label") }}</span>
477
+ <select
478
+ v-model.number="pageSize"
479
+ class="h-7 rounded-md border border-input bg-white px-1.5 text-sm tabular-nums focus:outline-none focus:ring-1 focus:ring-ring"
480
+ >
481
+ <option v-for="s in pageSizeOptions" :key="s" :value="s">{{ s }}</option>
482
+ </select>
483
+ </label>
484
+ </div>
485
+ <div v-if="lastPage > 0" class="flex items-center gap-2">
486
+ <Button
487
+ variant="outline"
488
+ size="sm"
489
+ class="h-7 text-sm"
490
+ :disabled="page === 0 || loading"
491
+ @click="prevPage"
492
+ >
493
+ {{ t("shared.datatable.footer.action.previous") }}
494
+ </Button>
495
+ <span class="tabular-nums">
496
+ {{ page + 1 }} / {{ lastPage + 1 }}
497
+ </span>
498
+ <Button
499
+ variant="outline"
500
+ size="sm"
501
+ class="h-7 text-sm"
502
+ :disabled="page >= lastPage || loading"
503
+ @click="nextPage"
504
+ >
505
+ {{ t("shared.datatable.footer.action.next") }}
506
+ </Button>
507
+ </div>
508
+ </div>
509
+ </div>
510
+ </template>
@@ -0,0 +1,87 @@
1
+ import type { DataTableColumn, DataTableFilter, DataTableRail, DataTableSavedViews, DataTableState, DataTableViewMode, RowKey, SortState } from './types.js';
2
+ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
3
+ props: __VLS_PrettifyLocal<Pick<Partial<{}> & Omit<{
4
+ readonly "onUpdate:sort"?: ((value: SortState) => any) | undefined;
5
+ readonly "onUpdate:page"?: ((value: number) => any) | undefined;
6
+ readonly "onUpdate:viewMode"?: ((value: string) => any) | undefined;
7
+ readonly "onUpdate:railOpen"?: ((value: boolean) => any) | undefined;
8
+ readonly onRowClick?: ((row: T) => any) | undefined;
9
+ readonly onStateChange?: ((state: DataTableState) => any) | undefined;
10
+ } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:sort" | "onUpdate:page" | "onUpdate:viewMode" | "onUpdate:railOpen" | "onRowClick" | "onStateChange"> & ({
11
+ data: T[];
12
+ columns: DataTableColumn<T>[];
13
+ rowKey: RowKey<T>;
14
+ search?: boolean | {
15
+ placeholder?: string;
16
+ };
17
+ columnsMenu?: boolean;
18
+ /** Mount the surface's own filter panel in the toolbar's Filter popover
19
+ * (`#filter-menu`). Pass an object to widen it / flag it as active. */
20
+ filter?: boolean | {
21
+ width?: string;
22
+ active?: boolean;
23
+ };
24
+ /** Built-in status/dimension filter pills (static mode). One dimension, or
25
+ * several (each renders as its own sub-menu). */
26
+ filterTabs?: DataTableFilter<T> | DataTableFilter<T>[];
27
+ /** Saved views control — the surface supplies snapshot/apply, the table
28
+ * owns the control, the storage and the dialogs. */
29
+ savedViews?: DataTableSavedViews | null;
30
+ /** View-mode switch (e.g. List | Gallery). Any mode other than `list`
31
+ * renders the `#view` slot in place of the table body. */
32
+ viewModes?: DataTableViewMode[] | null;
33
+ /** Collapse handle for the surface's left rail (see `ListSurface`). */
34
+ rail?: boolean | DataTableRail;
35
+ pagination?: boolean | {
36
+ pageSize?: number;
37
+ };
38
+ defaultSort?: {
39
+ column: string;
40
+ direction: "asc" | "desc";
41
+ };
42
+ serverMode?: boolean;
43
+ total?: number;
44
+ loading?: boolean;
45
+ emptyText?: string;
46
+ groupBy?: ((row: T) => string[]) | null;
47
+ groupEmptyLabel?: string;
48
+ } & {
49
+ sort?: SortState;
50
+ page?: number;
51
+ viewMode?: string;
52
+ railOpen?: boolean;
53
+ }) & Partial<{}>> & import("vue").PublicProps;
54
+ expose(exposed: import("vue").ShallowUnwrapRef<{}>): void;
55
+ attrs: any;
56
+ slots: {
57
+ [x: string]: ((props: {}) => any) | undefined;
58
+ } & {
59
+ [x: string]: ((props: {
60
+ row: any;
61
+ }) => any) | undefined;
62
+ } & {
63
+ 'filter-menu'?: (props: {}) => any;
64
+ } & {
65
+ 'toolbar-left'?: (props: {}) => any;
66
+ } & {
67
+ 'toolbar-right'?: (props: {}) => any;
68
+ } & {
69
+ scope?: (props: {}) => any;
70
+ } & {
71
+ actions?: (props: {}) => any;
72
+ } & {
73
+ view?: (props: {
74
+ rows: any;
75
+ loading: any;
76
+ }) => any;
77
+ } & {
78
+ empty?: (props: {}) => any;
79
+ };
80
+ emit: (((evt: "rowClick", row: T) => void) & ((evt: "stateChange", state: DataTableState) => void)) & (((evt: "update:sort", value: SortState) => void) & ((evt: "update:page", value: number) => void) & ((evt: "update:viewMode", value: string) => void) & ((evt: "update:railOpen", value: boolean) => void));
81
+ }>) => import("vue").VNode & {
82
+ __ctx?: Awaited<typeof __VLS_setup>;
83
+ };
84
+ export default _default;
85
+ type __VLS_PrettifyLocal<T> = {
86
+ [K in keyof T]: T[K];
87
+ } & {};
@@ -0,0 +1,21 @@
1
+ type __VLS_Props = {
2
+ /** Popover width class (a wide panel of inputs needs more than the default). */
3
+ width?: string;
4
+ /** Tint the trigger while at least one filter is set. */
5
+ active?: boolean;
6
+ };
7
+ declare var __VLS_34: {};
8
+ type __VLS_Slots = {} & {
9
+ default?: (props: typeof __VLS_34) => any;
10
+ };
11
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
12
+ active: boolean;
13
+ width: string;
14
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
16
+ export default _default;
17
+ type __VLS_WithSlots<T, S> = T & {
18
+ new (): {
19
+ $slots: S;
20
+ };
21
+ };
@@ -0,0 +1,46 @@
1
+ <script setup>
2
+ import { AltArrowDown, Filter } from "#solar-icons";
3
+ import {
4
+ Button,
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuLabel,
9
+ DropdownMenuSeparator,
10
+ DropdownMenuTrigger
11
+ } from "@revenexx/studio";
12
+ defineProps({
13
+ width: { type: String, required: false, default: "w-56" },
14
+ active: { type: Boolean, required: false, default: false }
15
+ });
16
+ const t = useT();
17
+ </script>
18
+
19
+ <template>
20
+ <DropdownMenu>
21
+ <DropdownMenuTrigger as-child>
22
+ <Button
23
+ variant="outline"
24
+ size="sm"
25
+ class="h-7 gap-1.5 text-sm"
26
+ :class="active ? 'border-primary/30 text-primary' : ''"
27
+ :aria-label="t('shared.datatable.filtermenu.aria')"
28
+ >
29
+ <Filter :size="14" />
30
+ {{ t("shared.datatable.filtermenu.action") }}
31
+ <AltArrowDown :size="12" class="opacity-60" />
32
+ </Button>
33
+ </DropdownMenuTrigger>
34
+ <DropdownMenuContent align="start" :class="width">
35
+ <DropdownMenuLabel class="text-sm text-muted-foreground">
36
+ {{ t("shared.datatable.filtermenu.heading") }}
37
+ </DropdownMenuLabel>
38
+ <DropdownMenuSeparator />
39
+ <slot>
40
+ <DropdownMenuItem disabled>
41
+ {{ t("shared.datatable.filtermenu.empty") }}
42
+ </DropdownMenuItem>
43
+ </slot>
44
+ </DropdownMenuContent>
45
+ </DropdownMenu>
46
+ </template>