@revenexx/studio-shared 0.1.1 → 0.2.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 (63) hide show
  1. package/README.md +22 -12
  2. package/dist/module.d.mts +9 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +27 -0
  5. package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +12 -0
  6. package/{components → dist/runtime/components}/DataTable/ColumnsMenu.vue +10 -17
  7. package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +12 -0
  8. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +59 -0
  9. package/dist/runtime/components/DataTable/DataTable.vue +453 -0
  10. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +59 -0
  11. package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +12 -0
  12. package/{components → dist/runtime/components}/DataTable/FilterMenu.vue +4 -8
  13. package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +12 -0
  14. package/dist/runtime/components/DataTable/types.d.ts +70 -0
  15. package/dist/runtime/components/DataTable/types.js +0 -0
  16. package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +30 -0
  17. package/{components → dist/runtime/components}/Shared/BuildLogsFlyout.vue +27 -45
  18. package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +30 -0
  19. package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +17 -0
  20. package/{components → dist/runtime/components}/Shared/GitDeploymentSettings.vue +22 -35
  21. package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +17 -0
  22. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +13 -0
  23. package/{components → dist/runtime/components}/Shared/KpiTile.vue +11 -19
  24. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +13 -0
  25. package/dist/runtime/components/Shared/PageHeader.d.vue.ts +23 -0
  26. package/{components → dist/runtime/components}/Shared/PageHeader.vue +6 -8
  27. package/dist/runtime/components/Shared/PageHeader.vue.d.ts +23 -0
  28. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +36 -0
  29. package/{components → dist/runtime/components}/Shared/SidebarNav.vue +15 -47
  30. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +36 -0
  31. package/dist/runtime/components/Shared/SortHeader.d.vue.ts +23 -0
  32. package/dist/runtime/components/Shared/SortHeader.vue +35 -0
  33. package/dist/runtime/components/Shared/SortHeader.vue.d.ts +23 -0
  34. package/dist/runtime/components/Shared/StateCard.d.vue.ts +32 -0
  35. package/{components → dist/runtime/components}/Shared/StateCard.vue +21 -36
  36. package/dist/runtime/components/Shared/StateCard.vue.d.ts +32 -0
  37. package/dist/runtime/components/Shared/StudioCard.d.vue.ts +15 -0
  38. package/dist/runtime/components/Shared/StudioCard.vue +19 -0
  39. package/dist/runtime/components/Shared/StudioCard.vue.d.ts +15 -0
  40. package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -0
  41. package/{components → dist/runtime/components}/Shared/TabBar.vue +7 -16
  42. package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -0
  43. package/dist/runtime/components/Shared/ViewTab.d.vue.ts +7 -0
  44. package/{components → dist/runtime/components}/Shared/ViewTab.vue +7 -9
  45. package/dist/runtime/components/Shared/ViewTab.vue.d.ts +7 -0
  46. package/dist/runtime/components/Shared/Wizard.d.vue.ts +47 -0
  47. package/{components → dist/runtime/components}/Shared/Wizard.vue +24 -41
  48. package/dist/runtime/components/Shared/Wizard.vue.d.ts +47 -0
  49. package/{composables/usePlatformAuth.ts → dist/runtime/composables/usePlatformAuth.d.ts} +3 -21
  50. package/dist/runtime/composables/usePlatformAuth.js +17 -0
  51. package/dist/runtime/composables/usePlatformTenant.d.ts +14 -0
  52. package/dist/runtime/composables/usePlatformTenant.js +18 -0
  53. package/dist/runtime/types/nav.d.ts +23 -0
  54. package/dist/runtime/types/nav.js +0 -0
  55. package/dist/types.d.mts +15 -0
  56. package/package.json +25 -12
  57. package/components/DataTable/DataTable.vue +0 -518
  58. package/components/DataTable/types.ts +0 -82
  59. package/components/Shared/SortHeader.vue +0 -50
  60. package/components/Shared/StudioCard.vue +0 -24
  61. package/composables/usePlatformTenant.ts +0 -35
  62. package/index.ts +0 -12
  63. package/nuxt.config.ts +0 -23
@@ -0,0 +1,453 @@
1
+ <script setup>
2
+ import { AltArrowDown, Filter, Magnifier as Search } 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
+ const props = defineProps({
19
+ data: { type: Array, required: true },
20
+ columns: { type: Array, required: true },
21
+ rowKey: { type: Function, required: true, skipCheck: true },
22
+ search: { type: [Boolean, Object], required: false, default: true },
23
+ columnsMenu: { type: Boolean, required: false, default: true },
24
+ filter: { type: Boolean, required: false, default: false },
25
+ filterTabs: { type: [Object, Array], required: false },
26
+ pagination: { type: [Boolean, Object], required: false, default: true },
27
+ defaultSort: { type: Object, required: false },
28
+ serverMode: { type: Boolean, required: false, default: false },
29
+ total: { type: Number, required: false },
30
+ loading: { type: Boolean, required: false, default: false },
31
+ emptyText: { type: String, required: false },
32
+ groupBy: { type: [Function, null], required: false, default: null },
33
+ groupEmptyLabel: { type: String, required: false }
34
+ });
35
+ const emit = defineEmits(["rowClick", "stateChange"]);
36
+ const query = ref("");
37
+ const sort = defineModel("sort", { type: Object, ...{
38
+ default: () => ({
39
+ column: null,
40
+ direction: null
41
+ })
42
+ } });
43
+ const page = defineModel("page", { type: Number, ...{ default: 0 } });
44
+ if (props.defaultSort && sort.value.column == null) {
45
+ sort.value = { column: props.defaultSort.column, direction: props.defaultSort.direction };
46
+ }
47
+ const pageSize = ref(
48
+ typeof props.pagination === "object" && props.pagination?.pageSize ? props.pagination.pageSize : 20
49
+ );
50
+ const pageSizeOptions = computed(() => Array.from(/* @__PURE__ */ new Set([10, 20, 50, 100, pageSize.value])).sort((a, b) => a - b));
51
+ const visibleKeys = ref(
52
+ new Set(props.columns.filter((c) => c.visible !== false).map((c) => c.key))
53
+ );
54
+ function toggleColumn(key) {
55
+ const next = new Set(visibleKeys.value);
56
+ if (next.has(key)) next.delete(key);
57
+ else next.add(key);
58
+ visibleKeys.value = next;
59
+ }
60
+ const visibleColumns = computed(
61
+ () => props.columns.filter((c) => visibleKeys.value.has(c.key))
62
+ );
63
+ const menuColumns = computed(() => props.columns);
64
+ function searchOf(col, row) {
65
+ if (col.searchGetter) return col.searchGetter(row).toLowerCase();
66
+ if (col.sortGetter) {
67
+ const v = col.sortGetter(row);
68
+ return v == null ? "" : String(v).toLowerCase();
69
+ }
70
+ const raw = row[col.key];
71
+ return raw == null ? "" : String(raw).toLowerCase();
72
+ }
73
+ const filterDims = computed(() => {
74
+ const raw = props.filterTabs;
75
+ if (!raw) return [];
76
+ const arr = Array.isArray(raw) ? raw : [raw];
77
+ return arr.map((f, i) => ({ key: f.key ?? f.label ?? `dim${i}`, label: f.label ?? "Status", getter: f.getter, options: f.options, allLabel: f.allLabel }));
78
+ });
79
+ const activeFilters = ref({});
80
+ const anyFilterActive = computed(() => Object.values(activeFilters.value).some(Boolean));
81
+ function setFilter(key, value) {
82
+ activeFilters.value = { ...activeFilters.value, [key]: value };
83
+ }
84
+ function dimCounts(d) {
85
+ const m = {};
86
+ for (const row of props.data) {
87
+ const k = String(d.getter(row) ?? "");
88
+ m[k] = (m[k] ?? 0) + 1;
89
+ }
90
+ return m;
91
+ }
92
+ const filtered = computed(() => {
93
+ if (props.serverMode || !filterDims.value.length) return props.data;
94
+ const active = filterDims.value.filter((d) => activeFilters.value[d.key]);
95
+ if (!active.length) return props.data;
96
+ return props.data.filter((row) => active.every((d) => String(d.getter(row) ?? "") === activeFilters.value[d.key]));
97
+ });
98
+ const searched = computed(() => {
99
+ if (props.serverMode) return props.data;
100
+ const q = query.value.trim().toLowerCase();
101
+ if (!q) return filtered.value;
102
+ return filtered.value.filter(
103
+ (row) => props.columns.some((col) => searchOf(col, row).includes(q))
104
+ );
105
+ });
106
+ function sortKey(col, row) {
107
+ if (col.sortGetter) return col.sortGetter(row);
108
+ const raw = row[col.key];
109
+ return raw;
110
+ }
111
+ const sorted = computed(() => {
112
+ if (props.serverMode) return searched.value;
113
+ const col = sort.value.column;
114
+ const dir = sort.value.direction;
115
+ if (!col || !dir) return searched.value;
116
+ const c = props.columns.find((x) => x.key === col);
117
+ if (!c) return searched.value;
118
+ const sign = dir === "asc" ? 1 : -1;
119
+ return [...searched.value].sort((a, b) => {
120
+ const av = sortKey(c, a);
121
+ const bv = sortKey(c, b);
122
+ const aNull = av == null || av === "";
123
+ const bNull = bv == null || bv === "";
124
+ if (aNull && bNull) return 0;
125
+ if (aNull) return 1;
126
+ if (bNull) return -1;
127
+ const aCmp = typeof av === "boolean" ? av ? 1 : 0 : av;
128
+ const bCmp = typeof bv === "boolean" ? bv ? 1 : 0 : bv;
129
+ if (aCmp < bCmp) return -1 * sign;
130
+ if (aCmp > bCmp) return 1 * sign;
131
+ return 0;
132
+ });
133
+ });
134
+ const totalCount = computed(
135
+ () => props.serverMode ? props.total ?? props.data.length : sorted.value.length
136
+ );
137
+ const paginated = computed(() => {
138
+ if (props.serverMode || !props.pagination) return sorted.value;
139
+ const start = page.value * pageSize.value;
140
+ return sorted.value.slice(start, start + pageSize.value);
141
+ });
142
+ const displayedRows = computed(() => props.groupBy ? sorted.value : paginated.value);
143
+ const keyedRows = computed(() => {
144
+ const seen = /* @__PURE__ */ new Map();
145
+ return displayedRows.value.map((row) => {
146
+ const base = rowKeyFor(row) ?? "\u2205";
147
+ const n = seen.get(base) ?? 0;
148
+ seen.set(base, n + 1);
149
+ return { row, key: n === 0 ? base : `${base}#${n}` };
150
+ });
151
+ });
152
+ const renderRows = computed(() => {
153
+ if (!props.groupBy) return keyedRows.value.map(({ row, key }) => ({ kind: "row", row, key }));
154
+ const emptyLabel = props.groupEmptyLabel ?? "Ungrouped";
155
+ const groups = /* @__PURE__ */ new Map();
156
+ for (const entry of keyedRows.value) {
157
+ const labels2 = props.groupBy(entry.row);
158
+ for (const label of labels2.length ? labels2 : [emptyLabel]) {
159
+ if (!groups.has(label)) groups.set(label, []);
160
+ groups.get(label).push(entry);
161
+ }
162
+ }
163
+ const labels = [...groups.keys()].sort((a, b) => a.localeCompare(b));
164
+ if (labels.includes(emptyLabel)) {
165
+ labels.splice(labels.indexOf(emptyLabel), 1);
166
+ labels.push(emptyLabel);
167
+ }
168
+ const out = [];
169
+ for (const label of labels) {
170
+ const rows = groups.get(label);
171
+ out.push({ kind: "header", label, count: rows.length, key: `__group__${label}` });
172
+ for (const e of rows) out.push({ kind: "row", row: e.row, key: `${label}::${e.key}` });
173
+ }
174
+ return out;
175
+ });
176
+ function emitState() {
177
+ emit("stateChange", {
178
+ search: query.value,
179
+ sort: { ...sort.value },
180
+ page: page.value,
181
+ pageSize: pageSize.value
182
+ });
183
+ }
184
+ watch([query, sort, page, pageSize], () => {
185
+ if (props.serverMode) emitState();
186
+ });
187
+ watch([query, sort, pageSize, activeFilters], () => {
188
+ page.value = 0;
189
+ }, { deep: true });
190
+ function toggleSort(column) {
191
+ if (sort.value.column !== column) {
192
+ sort.value = { column, direction: "asc" };
193
+ return;
194
+ }
195
+ if (sort.value.direction === "asc") {
196
+ sort.value = { column, direction: "desc" };
197
+ return;
198
+ }
199
+ sort.value = { column: null, direction: null };
200
+ }
201
+ function rowKeyFor(row) {
202
+ if (typeof props.rowKey === "function") return props.rowKey(row);
203
+ return row[props.rowKey];
204
+ }
205
+ const gridTemplate = computed(() => {
206
+ const tracks = visibleColumns.value.map((c) => c.width ?? "minmax(0,1fr)");
207
+ return `grid-template-columns: ${tracks.join(" ")};`;
208
+ });
209
+ const placeholder = computed(
210
+ () => typeof props.search === "object" ? props.search.placeholder ?? "Search\u2026" : "Search\u2026"
211
+ );
212
+ const slots = useSlots();
213
+ const showToolbar = computed(
214
+ () => props.search || props.columnsMenu || props.filter || !!props.filterTabs || !!slots.actions || !!slots["toolbar-left"]
215
+ );
216
+ function cellIconFor(col) {
217
+ return col.cellIcon ?? null;
218
+ }
219
+ const showFooter = computed(() => props.pagination !== false && !props.groupBy);
220
+ const lastPage = computed(
221
+ () => pageSize.value > 0 ? Math.max(0, Math.ceil(totalCount.value / pageSize.value) - 1) : 0
222
+ );
223
+ const rangeStart = computed(() => totalCount.value === 0 ? 0 : page.value * pageSize.value + 1);
224
+ const rangeEnd = computed(
225
+ () => Math.min((page.value + 1) * pageSize.value, totalCount.value)
226
+ );
227
+ function prevPage() {
228
+ if (page.value > 0) page.value -= 1;
229
+ }
230
+ function nextPage() {
231
+ if (page.value < lastPage.value) page.value += 1;
232
+ }
233
+ function defaultCell(col, row) {
234
+ const v = col.sortGetter ? col.sortGetter(row) : row[col.key];
235
+ if (v == null) return "\u2014";
236
+ return String(v);
237
+ }
238
+ </script>
239
+
240
+ <template>
241
+ <div class="rounded-lg border border-black/[0.06] overflow-hidden flex flex-col">
242
+ <!-- Toolbar ------------------------------------------------------ -->
243
+ <!-- Toolbar: LEFT = search · filter · columns · RIGHT = actions.
244
+ (Pagination always lives in the footer.) -->
245
+ <div
246
+ v-if="showToolbar"
247
+ class="flex items-center justify-between gap-2 px-4 py-3 bg-white/55 backdrop-blur-md"
248
+ >
249
+ <div class="flex items-center gap-2 min-w-0">
250
+ <div v-if="search" class="relative w-[21rem] max-w-full">
251
+ <Search :size="13" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
252
+ <Input
253
+ v-model="query"
254
+ :placeholder="placeholder"
255
+ class="h-7 pl-8 text-sm bg-white"
256
+ />
257
+ </div>
258
+ <!-- Generic "Filter" dropdown — each dimension is a labelled sub-menu. -->
259
+ <DropdownMenu v-if="filterDims.length">
260
+ <DropdownMenuTrigger as-child>
261
+ <Button
262
+ variant="outline"
263
+ size="sm"
264
+ class="h-7 gap-1.5 text-sm"
265
+ :class="anyFilterActive ? 'border-primary/30 text-primary' : ''"
266
+ >
267
+ <Filter :size="14" />
268
+ Filter
269
+ <AltArrowDown :size="12" class="opacity-60" />
270
+ </Button>
271
+ </DropdownMenuTrigger>
272
+ <DropdownMenuContent align="start" class="w-48">
273
+ <DropdownMenuSub v-for="d in filterDims" :key="d.key">
274
+ <DropdownMenuSubTrigger class="gap-2">
275
+ <span class="flex-1">{{ d.label }}</span>
276
+ <span v-if="activeFilters[d.key]" class="text-xs text-primary capitalize">{{ activeFilters[d.key] }}</span>
277
+ </DropdownMenuSubTrigger>
278
+ <DropdownMenuSubContent class="w-48">
279
+ <DropdownMenuItem class="gap-2" @select="setFilter(d.key, null)">
280
+ <span class="flex-1">{{ d.allLabel ?? "All" }}</span>
281
+ <span class="tabular-nums text-muted-foreground">{{ data.length }}</span>
282
+ </DropdownMenuItem>
283
+ <DropdownMenuSeparator />
284
+ <DropdownMenuItem
285
+ v-for="o in d.options"
286
+ :key="o.value"
287
+ class="gap-2"
288
+ @select="setFilter(d.key, o.value)"
289
+ >
290
+ <span class="flex-1">{{ o.label }}</span>
291
+ <span class="tabular-nums text-muted-foreground">{{ dimCounts(d)[o.value] ?? 0 }}</span>
292
+ </DropdownMenuItem>
293
+ </DropdownMenuSubContent>
294
+ </DropdownMenuSub>
295
+ </DropdownMenuContent>
296
+ </DropdownMenu>
297
+ <FilterMenu v-if="filter">
298
+ <slot name="filter-menu" />
299
+ </FilterMenu>
300
+ <ColumnsMenu
301
+ v-if="columnsMenu"
302
+ :columns="menuColumns"
303
+ :visible="visibleKeys"
304
+ @toggle="toggleColumn"
305
+ />
306
+ <slot name="toolbar-left" />
307
+ </div>
308
+ <div class="flex items-center gap-1.5 ml-auto shrink-0">
309
+ <slot name="actions" />
310
+ </div>
311
+ </div>
312
+
313
+ <!-- Header row -------------------------------------------------- -->
314
+ <div
315
+ 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"
316
+ :style="gridTemplate"
317
+ >
318
+ <div
319
+ v-for="col in visibleColumns"
320
+ :key="col.key"
321
+ :class="col.alignRight ? 'text-right' : ''"
322
+ >
323
+ <SortHeader
324
+ v-if="col.sortable"
325
+ :column="col.key"
326
+ :active="sort.column"
327
+ :direction="sort.direction"
328
+ :align-right="col.alignRight"
329
+ @toggle="toggleSort"
330
+ >
331
+ {{ col.label }}
332
+ </SortHeader>
333
+ <span v-else>{{ col.label }}</span>
334
+ </div>
335
+ </div>
336
+
337
+ <!-- Body --------------------------------------------------------- -->
338
+ <template v-for="entry in renderRows" :key="entry.key">
339
+ <!-- Group header (groupBy mode) -->
340
+ <div
341
+ v-if="entry.kind === 'header'"
342
+ 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"
343
+ >
344
+ {{ entry.label }}
345
+ <span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
346
+ </div>
347
+ <div
348
+ v-else
349
+ class="group grid items-center gap-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer bg-gray-50 transition-colors"
350
+ :style="gridTemplate"
351
+ @click="emit('rowClick', entry.row)"
352
+ >
353
+ <div
354
+ v-for="col in visibleColumns"
355
+ :key="col.key"
356
+ class="min-w-0"
357
+ :class="col.alignRight ? 'text-right' : ''"
358
+ >
359
+ <slot :name="`cell-${col.key}`" :row="entry.row">
360
+ <!-- Primary column: bold record anchor + optional tinted icon tile. -->
361
+ <div v-if="col.primary" class="flex items-center gap-2.5 min-w-0">
362
+ <span
363
+ v-if="cellIconFor(col)"
364
+ class="flex size-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"
365
+ >
366
+ <component :is="cellIconFor(col)" :size="13" />
367
+ </span>
368
+ <span class="text-sm font-semibold text-[#0B0522] truncate transition-colors group-hover:text-primary">{{ defaultCell(col, entry.row) }}</span>
369
+ </div>
370
+ <span v-else class="text-sm truncate">{{ defaultCell(col, entry.row) }}</span>
371
+ </slot>
372
+ </div>
373
+ </div>
374
+ </template>
375
+
376
+ <!-- Loading skeleton (only when there are no rows yet) ----------- -->
377
+ <template v-if="loading && !displayedRows.length">
378
+ <div
379
+ v-for="n in 6"
380
+ :key="`sk-${n}`"
381
+ class="grid items-center gap-2 border-t border-black/[0.04] bg-gray-50 px-4 py-3"
382
+ :style="gridTemplate"
383
+ >
384
+ <div
385
+ v-for="col in visibleColumns"
386
+ :key="col.key"
387
+ :class="col.alignRight ? 'flex justify-end' : ''"
388
+ >
389
+ <div
390
+ class="h-3 rounded bg-black/[0.07] animate-pulse"
391
+ :class="col.alignRight ? 'w-10' : 'w-2/3'"
392
+ />
393
+ </div>
394
+ </div>
395
+ </template>
396
+
397
+ <!-- Empty -------------------------------------------------------- -->
398
+ <div
399
+ v-if="!displayedRows.length && !loading"
400
+ class="px-3 py-8 text-center text-sm text-muted-foreground"
401
+ >
402
+ <slot name="empty">
403
+ {{ emptyText ?? "No entries." }}
404
+ </slot>
405
+ </div>
406
+
407
+ <!-- Footer ------------------------------------------------------- -->
408
+ <div
409
+ v-if="showFooter"
410
+ 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"
411
+ >
412
+ <div class="flex items-center gap-2.5">
413
+ <span class="tabular-nums">
414
+ <span v-if="totalCount === 0">No results</span>
415
+ <span v-else>Showing {{ rangeStart }}–{{ rangeEnd }} of {{ totalCount }}</span>
416
+ </span>
417
+ <span class="text-muted-foreground/40">-</span>
418
+ <label class="flex items-center gap-1.5">
419
+ <span class="hidden sm:inline">Rows per page</span>
420
+ <select
421
+ v-model.number="pageSize"
422
+ 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"
423
+ >
424
+ <option v-for="s in pageSizeOptions" :key="s" :value="s">{{ s }}</option>
425
+ </select>
426
+ </label>
427
+ </div>
428
+ <div v-if="lastPage > 0" class="flex items-center gap-2">
429
+ <Button
430
+ variant="outline"
431
+ size="sm"
432
+ class="h-7 text-sm"
433
+ :disabled="page === 0 || loading"
434
+ @click="prevPage"
435
+ >
436
+ Previous
437
+ </Button>
438
+ <span class="tabular-nums">
439
+ {{ page + 1 }} / {{ lastPage + 1 }}
440
+ </span>
441
+ <Button
442
+ variant="outline"
443
+ size="sm"
444
+ class="h-7 text-sm"
445
+ :disabled="page >= lastPage || loading"
446
+ @click="nextPage"
447
+ >
448
+ Next
449
+ </Button>
450
+ </div>
451
+ </div>
452
+ </div>
453
+ </template>
@@ -0,0 +1,59 @@
1
+ import type { DataTableColumn, DataTableFilter, DataTableState, 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 onRowClick?: ((row: T) => any) | undefined;
7
+ readonly onStateChange?: ((state: DataTableState) => any) | undefined;
8
+ } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:sort" | "onUpdate:page" | "onRowClick" | "onStateChange"> & ({
9
+ data: T[];
10
+ columns: DataTableColumn<T>[];
11
+ rowKey: RowKey<T>;
12
+ search?: boolean | {
13
+ placeholder?: string;
14
+ };
15
+ columnsMenu?: boolean;
16
+ filter?: boolean;
17
+ /** Built-in status/dimension filter pills (static mode). One dimension, or
18
+ * several (each renders as its own sub-menu). */
19
+ filterTabs?: DataTableFilter<T> | DataTableFilter<T>[];
20
+ pagination?: boolean | {
21
+ pageSize?: number;
22
+ };
23
+ defaultSort?: {
24
+ column: string;
25
+ direction: "asc" | "desc";
26
+ };
27
+ serverMode?: boolean;
28
+ total?: number;
29
+ loading?: boolean;
30
+ emptyText?: string;
31
+ groupBy?: ((row: T) => string[]) | null;
32
+ groupEmptyLabel?: string;
33
+ } & {
34
+ sort?: SortState;
35
+ page?: number;
36
+ }) & Partial<{}>> & import("vue").PublicProps;
37
+ expose(exposed: import("vue").ShallowUnwrapRef<{}>): void;
38
+ attrs: any;
39
+ slots: {
40
+ [x: string]: ((props: {
41
+ row: any;
42
+ }) => any) | undefined;
43
+ } & {
44
+ 'filter-menu'?: (props: {}) => any;
45
+ } & {
46
+ 'toolbar-left'?: (props: {}) => any;
47
+ } & {
48
+ actions?: (props: {}) => any;
49
+ } & {
50
+ empty?: (props: {}) => any;
51
+ };
52
+ emit: (((evt: "rowClick", row: T) => void) & ((evt: "stateChange", state: DataTableState) => void)) & (((evt: "update:sort", value: SortState) => void) & ((evt: "update:page", value: number) => void));
53
+ }>) => import("vue").VNode & {
54
+ __ctx?: Awaited<typeof __VLS_setup>;
55
+ };
56
+ export default _default;
57
+ type __VLS_PrettifyLocal<T> = {
58
+ [K in keyof T]: T[K];
59
+ } & {};
@@ -0,0 +1,12 @@
1
+ declare var __VLS_26: {};
2
+ type __VLS_Slots = {} & {
3
+ default?: (props: typeof __VLS_26) => any;
4
+ };
5
+ declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
+ export default _default;
8
+ type __VLS_WithSlots<T, S> = T & {
9
+ new (): {
10
+ $slots: S;
11
+ };
12
+ };
@@ -1,17 +1,13 @@
1
- <script setup lang="ts">
2
- import { Filter } from '#solar-icons'
1
+ <script setup>
2
+ import { Filter } from "#solar-icons";
3
3
  import {
4
4
  DropdownMenu,
5
5
  DropdownMenuContent,
6
6
  DropdownMenuItem,
7
7
  DropdownMenuLabel,
8
8
  DropdownMenuSeparator,
9
- DropdownMenuTrigger,
10
- } from '@revenexx/studio'
11
-
12
- // Placeholder filter affordance — exposes the icon + dropdown shell so
13
- // pages can mount custom filter chips inside via the default slot. Wire
14
- // up actual filter state at the call site.
9
+ DropdownMenuTrigger
10
+ } from "@revenexx/studio";
15
11
  </script>
16
12
 
17
13
  <template>
@@ -0,0 +1,12 @@
1
+ declare var __VLS_26: {};
2
+ type __VLS_Slots = {} & {
3
+ default?: (props: typeof __VLS_26) => any;
4
+ };
5
+ declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
+ export default _default;
8
+ type __VLS_WithSlots<T, S> = T & {
9
+ new (): {
10
+ $slots: S;
11
+ };
12
+ };
@@ -0,0 +1,70 @@
1
+ import type { Component } from 'vue';
2
+ export type Comparable = string | number | boolean | Date | null | undefined;
3
+ export interface DataTableColumn<T = unknown> {
4
+ /** Stable key — matches the slot name `cell-<key>` and is used in the
5
+ * sort state. */
6
+ key: string;
7
+ /** Header label. */
8
+ label: string;
9
+ /** CSS grid track. Defaults to `minmax(0,1fr)` for text columns; pass
10
+ * a fixed value like `120px` or `minmax(0,140px)` for compact ones. */
11
+ width?: string;
12
+ /** Enable sort affordance on this column. */
13
+ sortable?: boolean;
14
+ /** Returns the sort key for the row. Falls back to `row[key]` if it
15
+ * looks like a primitive. ISO date strings sort correctly as-is. */
16
+ sortGetter?: (row: T) => Comparable;
17
+ /** Returns the string a text-search query is matched against. Falls
18
+ * back to the stringified sortGetter result. */
19
+ searchGetter?: (row: T) => string;
20
+ /** Initial visibility (default `true`). */
21
+ visible?: boolean;
22
+ /** Hide-able via the column menu (default `true`). */
23
+ toggleable?: boolean;
24
+ /** Right-align cell content (numeric columns). */
25
+ alignRight?: boolean;
26
+ /** Optional icon component for the header. */
27
+ icon?: Component;
28
+ /** Marks the identifying column. When set (and no `#cell-<key>` slot is
29
+ * given) the DataTable renders the value as the bold record anchor —
30
+ * `font-semibold text-[#0B0522]`, truncated — optionally led by a tinted
31
+ * icon tile (see `cellIcon`). One per table. (datatable-ux) */
32
+ primary?: boolean;
33
+ /** Leading tinted icon tile for the cell (typically the `primary` column).
34
+ * A static icon component, rendered as the `bg-primary/10` rounded tile the
35
+ * scanning rules call for. (For a per-row icon use a `#cell-<key>` slot —
36
+ * `cellIcon` must NOT be a getter: Solar icons are functional components, so
37
+ * the table can't tell a getter from an icon and would crash invoking it.) */
38
+ cellIcon?: Component;
39
+ }
40
+ /** A built-in filter dimension rendered under the generic "Filter" dropdown as
41
+ * a labelled sub-menu (datatable-ux). Static mode only: the DataTable filters
42
+ * `data` in-memory and shows a count per option. Pass one, or an array of
43
+ * several dimensions — each becomes its own sub-menu. */
44
+ export interface DataTableFilter<T = unknown> {
45
+ /** Stable id for this dimension (defaults to `label`). */
46
+ key?: string;
47
+ /** Sub-menu title, e.g. "Status" (default `Status`). */
48
+ label?: string;
49
+ /** The row's value for this dimension (e.g. `r => r.status`). */
50
+ getter: (row: T) => string | null | undefined;
51
+ /** The selectable values + their labels. */
52
+ options: {
53
+ value: string;
54
+ label: string;
55
+ }[];
56
+ /** Label for the reset/"all" entry (default `All`). */
57
+ allLabel?: string;
58
+ }
59
+ export type SortDirection = 'asc' | 'desc' | null;
60
+ export interface SortState {
61
+ column: string | null;
62
+ direction: SortDirection;
63
+ }
64
+ export interface DataTableState {
65
+ search: string;
66
+ sort: SortState;
67
+ page: number;
68
+ pageSize: number;
69
+ }
70
+ export type RowKey<T> = keyof T | ((row: T) => string | number);
File without changes
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Minimal shape of a site deployment — just the fields this flyout renders.
3
+ * Kept local so studio-shared has no dependency on the private SDK; a full
4
+ * SDK `Models.Deployment` is structurally assignable to it.
5
+ */
6
+ interface BuildDeployment {
7
+ $id?: string;
8
+ status?: string;
9
+ buildLogs?: string;
10
+ buildDuration?: number;
11
+ providerBranch?: string;
12
+ providerCommitHash?: string;
13
+ providerCommitUrl?: string;
14
+ }
15
+ type __VLS_Props = {
16
+ open: boolean;
17
+ deployment: BuildDeployment | null;
18
+ loading: boolean;
19
+ isBuilding?: boolean;
20
+ userPinned?: boolean;
21
+ error?: string | null;
22
+ };
23
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
24
+ "update:open": (args_0: boolean) => any;
25
+ refresh: () => any;
26
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
27
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
28
+ onRefresh?: (() => any) | undefined;
29
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
+ export default _default;